diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 9c8b3da6..00000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -.vscode -target -.gitignore -docker-compose.yml -Dockerfile -.dockerignore -node_modules -.github diff --git a/.gitignore b/.gitignore index df65775d..ccadbf92 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ dist vendor/ vender/ -core/src/game - # Created by https://www.toptal.com/developers/gitignore/api/git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all # Edit at https://www.toptal.com/developers/gitignore?templates=git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all diff --git a/.rustfmt.toml b/.rustfmt.toml index b9b2652a..63ac28ed 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -3,8 +3,7 @@ hard_tabs = true newline_style = "Unix" use_small_heuristics = "Default" reorder_imports = true -reorder_imported_names = true -imports_granularity = "Crate" +imports_granularity = "Module" reorder_modules = true remove_nested_parens = true edition = "2021" @@ -12,3 +11,4 @@ merge_derives = true use_try_shorthand = false use_field_init_shorthand = false force_explicit_abi = true +unstable_features = true diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index a8181dec..ba34b132 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -9,15 +9,15 @@ OneLauncher - Open source Rust-based Minecraft Launcher Copyright (C) 2023~2024 Polyfrost and Contributors This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, version 3. +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. +GNU Affero General Public License for more details. -You should have received a copy of the GNU General Public License +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . ``` diff --git a/Cargo.toml b/Cargo.toml index ea7e31dc..3bb15d4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "debugger", "desktop/src-tauri", "testing", - "backend" ] [workspace.package] diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0aef2f13..00000000 --- a/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM rustlang/rust:nightly-alpine as builder - -WORKDIR /usr/src/polyfrost-api - -# Use sparse registry because it is significantly faster and this already requires nightly -ENV CARGO_UNSTABLE_SPARSE_REGISTRY=true - -# Install build tools -RUN apk add --no-cache g++ - -# Copy source files -COPY . . - -# Build actual binary -RUN cargo build --package polyfrost_api --release --locked --all-features - -# --------------------------------------------------------------------------------------------- - -# :3 :3 :3 -FROM alpine:3 - -COPY --from=builder /usr/src/polyfrost-api/target/release/polyfrost_api /usr/local/bin/polyfrost_api - -# Use an unprivileged user -RUN adduser --home /nonexistent --no-create-home --disabled-password polyfrost-api -USER polyfrost-api - -HEALTHCHECK --interval=10s --timeout=3s --retries=5 CMD wget --spider --q http://localhost:$PORT/ || exit 1 - -CMD ["polyfrost_api"] diff --git a/LICENSE b/LICENSE index f288702d..5e3e69b5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,40 +633,28 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/backend/Cargo.toml b/backend/Cargo.toml deleted file mode 100644 index 9fc4817a..00000000 --- a/backend/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "polyfrost_api" -version = "0.0.0" -description = "API and Backend for Polyfrost" -license = { workspace = true } -edition = { workspace = true } -repository = { workspace = true } -documentation = { workspace = true } -readme = { workspace = true } -homepage = { workspace = true } -authors = { workspace = true } - -[[bin]] -name = "polyfrost_api" -path = "src/main.rs" - -[dependencies] -actix-web = { version = "4" } -actix-rt = { version = "2.9.0" } -governor = { version = "0.6" } - -tokio = { workspace = true } -tokio-stream = { workspace = true } -futures = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -reqwest = { workspace = true } -thiserror = { workspace = true } -anyhow = { workspace = true } - -log = { version = "0.4" } -env_logger = { version = "0.11" } -dotenvy = { version = "0.15" } -xml = { version = "0.8" } diff --git a/backend/src/lib.rs b/backend/src/lib.rs deleted file mode 100644 index e99ffe2a..00000000 --- a/backend/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::path::Path; - -use actix_web::{get, web::Data, HttpResponse, Responder}; - -#[get("/")] -pub async fn index() -> impl Responder { - HttpResponse::Ok().body("Polyfrost API") -} - -#[get("/oneconfig/{version}-{loader}")] -pub async fn oneconfig( - data: Data, - path: Path<(String, String)>, -) -> impl Responder { -} diff --git a/backend/src/main.rs b/backend/src/main.rs deleted file mode 100644 index a1c25716..00000000 --- a/backend/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -use actix_web::{web::Data, App, HttpServer}; -use log::info; - -#[actix_rt::main] -async fn main() -> std::io::Result<()> { - dotenvy::dotenv().ok(); - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - - info!( - "starting polyfrost api on {}", - dotenvy::var("BIND_ADDR").unwrap() - ); - - HttpServer::new(|| { - let mut headers = reqwest::header::HeaderMap::new(); - let header = reqwest::header::HeaderValue::from_str(&format!( - "onelauncher/{} (polyfrost.org)", - env!("CARGO_PKG_VERSION") - )) - .unwrap(); - headers.insert(reqwest::header::USER_AGENT, header); - - App::new() - .app_data(Data::new(state::AppState { - public_maven_url: dotenvy::var("PUBLIC_MAVEN_URL") - .unwrap_or("https://repo.polyfrost.org".to_string()), - internal_maven_url: dotenvy::var("INTERNAL_MAVEN_URL") - .unwrap_or("http://localhost:8000".to_string()), - http_client: reqwest::Client::builder() - .tcp_keepalive(Some(std::time::Duration::from_secs(15))) - .default_headers(headers) - .build() - .expect("failed to build reqwest client!"), - })) - .service(index) - .service(oneconfig) - }) - .bind(dotenvy::var("BIND_ADDR").unwrap())? - .run() - .await -} diff --git a/backend/src/parsing.rs b/backend/src/parsing.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/src/state.rs b/backend/src/state.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/src/utils.rs b/backend/src/utils.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/core/src/api/cluster/create.rs b/core/src/api/cluster/create.rs index e69de29b..d9bcfea3 100644 --- a/core/src/api/cluster/create.rs +++ b/core/src/api/cluster/create.rs @@ -0,0 +1,266 @@ +//! Utilities for creating Clusters + +use crate::data::Loader; +use crate::package::from::CreatePackCluster; +use crate::proxy::send::send_cluster; +use crate::proxy::ClusterPayloadType; +pub use crate::store::{Cluster, ClusterPath, JavaOptions, PackageData, State}; +use crate::utils::io::{self, canonicalize}; +use crate::{cluster, package}; +use interpulse::api::modded::LoaderVersion; +use std::path::PathBuf; + +/// Creates a [`Cluster`] and adds it to the memory [`State`]. +/// Returns a relative filepath ([`ClusterPath`]) which can be used to access the cluster. +#[tracing::instrument] +#[onelauncher_debug::debugger] +#[allow(clippy::too_many_arguments)] +pub async fn create_cluster( + mut name: String, + mc_version: String, + mod_loader: Loader, + loader_version: Option, + icon: Option, + icon_url: Option, + package_data: Option, + skip: Option, + skip_watch: Option, +) -> crate::Result { + name = cluster::sanitize_cluster_name(&name); + tracing::trace!("creating new cluster {}", name); + let state = State::get().await?; + let uuid = uuid::Uuid::new_v4(); + let mut path = state.directories.clusters_dir().await.join(&name); + + // i hate it here + if path.exists() { + let mut new_name; + let mut new_path; + let mut which = 1; + loop { + new_name = format!("{name} ({which})"); + new_path = state.directories.clusters_dir().await.join(&new_name); + if !new_path.exists() { + break; + } + which += 1; + } + + tracing::debug!( + "collision while creating new cluster: {}, renaming to {}", + path.display(), + new_path.display() + ); + path = new_path; + name = new_name; + } + + io::create_dir_all(&path).await?; + tracing::info!( + "creating cluster at path {}", + &canonicalize(&path)?.display() + ); + let loader = if mod_loader != Loader::Vanilla { + get_loader_version(mc_version.clone(), mod_loader, loader_version).await? + } else { + None + }; + + let mut cluster = Cluster::new(uuid, name, mc_version).await?; + let result = async { + if let Some(ref icon) = icon { + let bytes = io::read(state.directories.caches_dir().await.join(icon)).await?; + cluster + .set_icon( + &state.directories.caches_dir().await, + &state.io_semaphore, + bytes::Bytes::from(bytes), + &icon.to_string_lossy(), + ) + .await?; + } + + cluster.meta.icon_url = icon_url; + if let Some(loader_version) = loader { + cluster.meta.loader = mod_loader; + cluster.meta.loader_version = Some(loader_version); + } + + cluster.meta.package_data = package_data; + if let Some(package_data) = &mut cluster.meta.package_data { + package_data.locked = + Some(package_data.package_id.is_some() && package_data.version_id.is_some()); + } + + send_cluster( + uuid, + &cluster.cluster_path(), + &cluster.meta.name, + ClusterPayloadType::Created, + ) + .await?; + + { + let mut clusters = state.clusters.write().await; + clusters + .insert(cluster.clone(), skip_watch.unwrap_or_default()) + .await?; + } + + if !skip.unwrap_or(false) { + crate::game::install_minecraft(&cluster, None, false).await?; + } + + State::sync().await?; + + Ok(cluster.cluster_path()) + } + .await; + + match result { + Ok(cluster) => Ok(cluster), + Err(err) => { + let _ = crate::api::cluster::remove(&cluster.cluster_path()).await; + + Err(err) + } + } +} + +/// Create a [`Cluster`] from a [`CreatePackCluster`] cluster, returning a [`ClusterPath`]. +pub async fn create_cluster_from_package(cluster: CreatePackCluster) -> crate::Result { + create_cluster( + cluster.name, + cluster.mc_version, + cluster.mod_loader, + cluster.loader_version, + cluster.icon, + cluster.icon_url, + cluster.package_data, + cluster.skip, + cluster.skip_watch, + ) + .await +} + +/// Create a duplicate [`Cluster`] from another [`ClusterPath`], returning a new [`ClusterPath`] +pub async fn create_cluster_from_duplicate(from: ClusterPath) -> crate::Result { + let cluster = cluster::get(&from, None).await?.ok_or_else(|| { + anyhow::anyhow!("failed to get unmanaged cluster from {}", from.to_string()) + })?; + let cluster_path = create_cluster( + cluster.meta.name.clone(), + cluster.meta.mc_version.clone(), + cluster.meta.loader, + cluster.meta.loader_version.clone().map(|it| it.id), + cluster.meta.icon.clone(), + cluster.meta.icon_url.clone(), + cluster.meta.package_data.clone(), + Some(true), + Some(true), + ) + .await?; + + let state = State::get().await?; + let copied = package::import::copy_minecraft( + cluster_path.clone(), + from.full_path().await?, + &state.io_semaphore, + None, + ) + .await?; + + let duplicated = cluster::get(&cluster_path, None).await?.ok_or_else(|| { + anyhow::anyhow!( + "failed to get unmanaged cluster from {}", + cluster_path.to_string() + ) + })?; + + crate::game::install_minecraft(&duplicated, Some(copied), false).await?; + { + let state = State::get().await?; + let mut watcher = state.watcher.write().await; + Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?; + } + + send_cluster( + cluster.uuid, + &cluster.cluster_path(), + &cluster.meta.name, + ClusterPayloadType::Edited, + ) + .await?; + + State::sync().await?; + Ok(cluster_path) +} + +/// Get the latest [`LoaderVersion`] from a [`Loader`]. +#[tracing::instrument] +#[onelauncher_debug::debugger] +pub(crate) async fn get_loader_version( + mc_version: String, + loader: Loader, + loader_version: Option, +) -> crate::Result> { + let state = State::get().await?; + let metadata = state.metadata.read().await; + let version = loader_version.unwrap_or_else(|| "latest".to_string()); + let filter = |it: &LoaderVersion| match version.as_str() { + "latest" => true, + "stable" => it.stable, + id => { + it.id == *id + || format!("{}-{}", mc_version, id) == it.id + || format!("{}-{}-{}", mc_version, id, mc_version) == it.id + } + }; + + let loader_meta = match loader { + Loader::Forge => &metadata.forge, + Loader::Fabric => &metadata.fabric, + Loader::Quilt => &metadata.quilt, + Loader::NeoForge => &metadata.neoforge, + Loader::LegacyFabric => &metadata.legacy_fabric, + _ => return Err(CreateClusterError::MissingManifest(loader.to_string()).into()), + }; + + let loaders = &loader_meta + .game_versions + .iter() + .find(|it| { + it.id + .replace(interpulse::api::modded::DUMMY_REPLACE_STRING, &mc_version) + == mc_version + }) + .ok_or_else(|| { + CreateClusterError::UnsupportedLoader(loader.to_string(), mc_version.clone()) + })? + .loaders; + + let loader_version = loaders + .iter() + .find(|&it| filter(it)) + .cloned() + .or(if version == "stable" { + loaders.iter().next().cloned() + } else { + None + }) + .ok_or_else(|| CreateClusterError::InvalidLoaderVersion(version, loader.to_string()))?; + + Ok(Some(loader_version)) +} + +#[derive(thiserror::Error, Debug)] +pub enum CreateClusterError { + #[error("Loader {0} is unsupported on Minecraft version {1}")] + UnsupportedLoader(String, String), + #[error("Invalid Loader version {0} for Loader {1}")] + InvalidLoaderVersion(String, String), + #[error("Failed to get Loader manifest for {0}.")] + MissingManifest(String), + #[error("Failed to handle I/O operations: {0}")] + IOError(#[from] std::io::Error), +} diff --git a/core/src/api/cluster/mod.rs b/core/src/api/cluster/mod.rs index d3682628..54accfef 100644 --- a/core/src/api/cluster/mod.rs +++ b/core/src/api/cluster/mod.rs @@ -1,15 +1,31 @@ -//! OneLauncher cluster management +//! **OneLauncher Cluster** +//! +//! API for our managed Minecraft instances, Clusters. -use crate::proxy::send::send_cluster; -use crate::proxy::ClusterPayloadType; -use crate::prelude::ClusterPath; -use crate::store::{State, PackagePath}; -use crate::utils::io; -pub use crate::store::{JavaOptions, Cluster}; +use crate::proxy::send::{init_ingress, send_cluster, send_ingress}; +use crate::proxy::utils::ingress_try_for_each; +use crate::proxy::{ClusterPayloadType, IngressType}; +use crate::store::ProcessorChild; +// use crate::package::from::{EnvType, PackDependency, PackFile, PackFileHash, PackFormat}; +use crate::prelude::{ClusterPath, JavaVersion, PackagePath}; +pub use crate::store::{Cluster, JavaOptions, State}; +use crate::store::{InnerPathLinux, MinecraftCredentials, PackageMetadata, PackageSide}; +use crate::utils::http; +use crate::utils::io::{self, IOError}; -use std::collections::HashMap; +use async_zip::tokio::write::ZipFileWriter; +use async_zip::{Compression, ZipEntryBuilder}; +use serde_json::json; + +use std::collections::{HashMap, HashSet}; use std::future::Future; -use std::path::PathBuf; +use std::iter::FromIterator; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::File; +use tokio::io::AsyncReadExt; +use tokio::process::Command; +use tokio::sync::RwLock; pub mod create; pub mod update; @@ -17,117 +33,348 @@ pub mod update; /// get a cluster by its specified [`ClusterPath`]. #[tracing::instrument] pub async fn get(path: &ClusterPath, clear: Option) -> crate::Result> { - let state = State::get().await?; - let clusters = state.clusters.read().await; - let mut cluster = clusters.0.get(path).cloned(); + let state = State::get().await?; + let clusters = state.clusters.read().await; + let mut cluster = clusters.0.get(path).cloned(); - if clear.unwrap_or(false) { - if let Some(cluster) = &mut cluster { - cluster.packages = HashMap::new(); - } - } + if clear.unwrap_or(false) { + if let Some(cluster) = &mut cluster { + cluster.packages = HashMap::new(); + } + } - Ok(cluster) + Ok(cluster) +} + +/// get a list of all [`Cluster`]s +#[tracing::instrument] +pub async fn list(clear: Option) -> crate::Result> { + let state = State::get().await?; + let clusters = state.clusters.read().await; + Ok(clusters + .0 + .clone() + .into_iter() + .map(|mut it| { + if clear.unwrap_or(false) { + it.1.packages = HashMap::new(); + } + it + }) + .collect()) +} + +/// run a Minecraft [`Cluster`] using the default credentials. +#[tracing::instrument] +pub async fn run(path: &ClusterPath) -> crate::Result>> { + let state = State::get().await?; + let default = { + let mut users = state.users.write().await; + users + .get_default() + .await? + .ok_or_else(|| anyhow::anyhow!("no default credentials found!"))? + }; + + run_credentials(path, &default).await +} + +/// run a Minecraft [`Cluster`] using [`MinecraftCredentials`] for authentication. +/// returns an [`Arc`] pointer to [`RwLock`] to [`ProcessorChild`]. +#[tracing::instrument(skip(creds))] +#[onelauncher_debug::debugger] +pub async fn run_credentials( + path: &ClusterPath, + creds: &MinecraftCredentials, +) -> crate::Result>> { + let state = State::get().await?; + let settings = state.settings.read().await; + let cluster = get(path, None) + .await? + .ok_or_else(|| anyhow::anyhow!("failed to run a nonexistent cluster at path {}", path))?; + + let pre = &cluster + .init_hooks + .as_ref() + .unwrap_or(&settings.init_hooks) + .pre; + if let Some(hook) = pre { + // TODO: hook parameters + let mut cmd = hook.split(' '); + if let Some(command) = cmd.next() { + let full_path = path.full_path().await?; + let result = Command::new(command) + .args(&cmd.collect::>()) + .current_dir(&full_path) + .spawn() + .map_err(|e| IOError::with_path(e, &full_path))? + .wait() + .await + .map_err(IOError::from)?; + + if !result.success() { + return Err(anyhow::anyhow!( + "non-zero exit code for pre-launch hook: {}", + result.code().unwrap_or(-1) + ) + .into()); + } + } + } + + let java_args = cluster + .java + .as_ref() + .and_then(|it| it.custom_arguments.as_ref()) + .unwrap_or(&settings.custom_java_args); + let wrapper = cluster + .init_hooks + .as_ref() + .map_or(&settings.init_hooks.wrapper, |it| &it.wrapper); + let memory = cluster.memory.unwrap_or(settings.memory); + let resolution = cluster.resolution.unwrap_or(settings.resolution); + let env_args = cluster + .java + .as_ref() + .and_then(|it| it.custom_env_arguments.as_ref()) + .unwrap_or(&settings.custom_env_args); + let post = cluster + .init_hooks + .as_ref() + .unwrap_or(&settings.init_hooks) + .post + .clone(); + let mut mc_options: Vec<(String, String)> = vec![]; + if let Some(fullscreen) = cluster.force_fullscreen { + mc_options.push(("fullscreen".to_string(), fullscreen.to_string())); + } else if settings.force_fullscreen { + mc_options.push(("fullscreen".to_string(), "true".to_string())); + } + + let process = crate::game::launch_minecraft( + &cluster, + java_args, + env_args, + &mc_options, + post, + creds, + &resolution, + &memory, + wrapper, + ) + .await?; + + Ok(process) } /// remove a specified cluster from it's [`ClusterPath`]. #[tracing::instrument] pub async fn remove(path: &ClusterPath) -> crate::Result<()> { - let state = State::get().await?; - let mut clusters = state.clusters.write().await; + let state = State::get().await?; + let mut clusters = state.clusters.write().await; - if let Some(cluster) = clusters.remove(path).await? { - send_cluster(cluster.uuid, path, &cluster.meta.name, ClusterPayloadType::Deleted).await?; - } + if let Some(cluster) = clusters.remove(path).await? { + send_cluster( + cluster.uuid, + path, + &cluster.meta.name, + ClusterPayloadType::Deleted, + ) + .await?; + } - Ok(()) + Ok(()) } /// get a cluster by it's [`uuid::Uuid`] #[tracing::instrument] -pub async fn get_by_uuid( - uuid: uuid::Uuid, - clear: Option, -) -> crate::Result> { - let state = State::get().await?; - let clusters = state.clusters.read().await; - let mut cluster = clusters.0.values().find(|c| c.uuid == uuid).cloned(); +pub async fn get_by_uuid(uuid: uuid::Uuid, clear: Option) -> crate::Result> { + let state = State::get().await?; + let clusters = state.clusters.read().await; + let mut cluster = clusters.0.values().find(|c| c.uuid == uuid).cloned(); - if clear.unwrap_or(false) { - if let Some(cluster) = &mut cluster { - cluster.packages = HashMap::new(); - } - } + if clear.unwrap_or(false) { + if let Some(cluster) = &mut cluster { + cluster.packages = HashMap::new(); + } + } - Ok(cluster) + Ok(cluster) } /// get a cluster's full path by it's [`ClusterPath`]. #[tracing::instrument] pub async fn get_full_path(path: &ClusterPath) -> crate::Result { - let _ = get(path, Some(true)).await?.ok_or_else(|| anyhow::anyhow!("failed to get the full path of profile at path {}", path))?; - let full_path = io::canonicalize(path.full_path().await?)?; + let _ = get(path, Some(true)).await?.ok_or_else(|| { + anyhow::anyhow!("failed to get the full path of profile at path {}", path) + })?; + let full_path = io::canonicalize(path.full_path().await?)?; - Ok(full_path) + Ok(full_path) } /// get a specific mod's full path in the filesystem by it's [`ClusterPath`] and [`PackagePath`]. #[tracing::instrument] pub async fn get_mod_path( - cluster_path: &ClusterPath, - package_path: &PackagePath, + cluster_path: &ClusterPath, + package_path: &PackagePath, ) -> crate::Result { - if get(cluster_path, Some(true)).await?.is_some() { - let full_path = io::canonicalize(package_path.full_path(cluster_path.clone()).await?)?; + if get(cluster_path, Some(true)).await?.is_some() { + let full_path = io::canonicalize(package_path.full_path(cluster_path.clone()).await?)?; - return Ok(full_path); - } + return Ok(full_path); + } - Err(anyhow::anyhow!("failed to get the full path of a cluster at path {}", package_path.full_path(cluster_path.clone()).await?.display()).into()) + Err(anyhow::anyhow!( + "failed to get the full path of a cluster at path {}", + package_path + .full_path(cluster_path.clone()) + .await? + .display() + ) + .into()) } /// edit a cluster with an async closure and it's [`ClusterPath`] pub async fn edit( - path: &ClusterPath, - action: impl Fn(&mut Cluster) -> FutFn, + path: &ClusterPath, + action: impl Fn(&mut Cluster) -> FutFn, ) -> crate::Result<()> where - FutFn: Future>, + FutFn: Future>, { - let state = State::get().await?; - let mut clusters = state.clusters.write().await; + let state = State::get().await?; + let mut clusters = state.clusters.write().await; - match clusters.0.get_mut(path) { - Some(ref mut cluster) => { - action(cluster).await?; + match clusters.0.get_mut(path) { + Some(ref mut cluster) => { + action(cluster).await?; - send_cluster(cluster.uuid, path, &cluster.meta.name, ClusterPayloadType::Edited).await?; + send_cluster( + cluster.uuid, + path, + &cluster.meta.name, + ClusterPayloadType::Edited, + ) + .await?; - Ok(()) - } - None => Err(anyhow::anyhow!("unmanaged profile edited at {}", path.to_string()).into()) - } + Ok(()) + } + None => Err(anyhow::anyhow!("unmanaged profile edited at {}", path.to_string()).into()), + } } -/// Try to update a Cluster's playtime. +/// update a [`Cluster`]'s icon +pub async fn edit_icon(path: &ClusterPath, icon_path: Option<&Path>) -> crate::Result<()> { + let state = State::get().await?; + let result = if let Some(icon) = icon_path { + let bytes = io::read(icon).await?; + let mut clusters = state.clusters.write().await; + match clusters.0.get_mut(path) { + Some(ref mut cluster) => { + cluster + .set_icon( + &state.directories.caches_dir().await, + &state.io_semaphore, + bytes::Bytes::from(bytes), + &icon.to_string_lossy(), + ) + .await?; + + send_cluster( + cluster.uuid, + path, + &cluster.meta.name, + ClusterPayloadType::Edited, + ) + .await?; + Ok(()) + } + None => Err(anyhow::anyhow!( + "failed to update unmanaged cluster at {}", + path.to_string() + ) + .into()), + } + } else { + edit(path, |cluster| { + cluster.meta.icon = None; + async { Ok(()) } + }) + .await?; + State::sync().await?; + + Ok(()) + }; + + State::sync().await?; + result +} + +/// gets the optimal java version for a given [`Cluster`]. +pub async fn get_optimal_java_version(path: &ClusterPath) -> crate::Result> { + let state = State::get().await?; + if let Some(cluster) = get(path, None).await? { + let metadata = state.metadata.read().await; + let version = metadata + .minecraft + .versions + .iter() + .find(|it| it.id == cluster.meta.mc_version) + .ok_or_else(|| { + anyhow::anyhow!( + "invalid or unknown Minecraft version {}", + cluster.meta.mc_version + ) + })?; + + let version_info = crate::game::metadata::download_version_info( + &state, + version, + cluster.meta.loader_version.as_ref(), + None, + None, + ) + .await?; + + let version = crate::game::java_version_from_cluster(&cluster, &version_info).await?; + + Ok(version) + } else { + Err(anyhow::anyhow!( + "failed to get the java version of unmanaged cluster at {}", + path.to_string() + ) + .into()) + } +} + +/// Try to update a [`Cluster`]'s playtime. #[tracing::instrument] #[onelauncher_debug::debugger] pub async fn update_playtime(path: &ClusterPath) -> crate::Result<()> { - let state = State::get().await?; - let cluster = get(path, None).await?.ok_or_else(|| anyhow::anyhow!("failed to update playtime at path {}", path))?; - let recent_playtime = cluster.meta.recently_played; - - /* - * todo - */ + let state = State::get().await?; + let cluster = get(path, None) + .await? + .ok_or_else(|| anyhow::anyhow!("failed to update playtime at path {}", path))?; + let recent_playtime = cluster.meta.recently_played; - let mut clusters = state.clusters.write().await; - if let Some(cluster) = clusters.0.get_mut(path) { - cluster.meta.overall_played += recent_playtime; - cluster.meta.recently_played = 0; - } + /* + * todo + */ - State::sync().await?; + let mut clusters = state.clusters.write().await; + if let Some(cluster) = clusters.0.get_mut(path) { + cluster.meta.overall_played += recent_playtime; + cluster.meta.recently_played = 0; + } - Ok(()) + State::sync().await?; + + Ok(()) +} + +/// Sanitize a user-inputted [`Cluster`] name. +pub fn sanitize_cluster_name(input: &str) -> String { + input.replace(['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>'], "_") } diff --git a/core/src/api/cluster/update.rs b/core/src/api/cluster/update.rs index e69de29b..8e60c619 100644 --- a/core/src/api/cluster/update.rs +++ b/core/src/api/cluster/update.rs @@ -0,0 +1 @@ +//! Utilities for updating Clusters diff --git a/core/src/api/handler.rs b/core/src/api/handler.rs index 7a104af6..378b93fb 100644 --- a/core/src/api/handler.rs +++ b/core/src/api/handler.rs @@ -1,42 +1,42 @@ use std::path::PathBuf; +use crate::api::prelude::InternetPayload; use crate::proxy::send::{send_internet, send_message}; use crate::utils::io; -use crate::api::prelude::InternetPayload; pub async fn parse_uri(cmd: &str) -> crate::Result<()> { - let cmd = handle_cmd(cmd).await?; - send_internet(cmd).await?; - Ok(()) + let cmd = handle_cmd(cmd).await?; + send_internet(cmd).await?; + Ok(()) } pub async fn handle_cmd(cmd: &str) -> crate::Result { - tracing::debug!("parsing command {}", &cmd); + tracing::debug!("parsing command {}", &cmd); - if let Some(sub) = cmd.strip_prefix("onelauncher://") { - Ok(handle_uri(sub).await?) - } else { - let path = io::canonicalize(PathBuf::from(cmd))?; - if let Some(extension) = path.extension() { - if extension == "zip" { - return Ok(InternetPayload::InstallPath { path }) - } - } - send_message(&format!("invalid command filetype {}", path.display())).await?; - Err(anyhow::anyhow!("invalid command filename {}", path.display()).into()) - } + if let Some(sub) = cmd.strip_prefix("onelauncher://") { + Ok(handle_uri(sub).await?) + } else { + let path = io::canonicalize(PathBuf::from(cmd))?; + if let Some(extension) = path.extension() { + if extension == "zip" { + return Ok(InternetPayload::InstallPath { path }); + } + } + send_message(&format!("invalid command filetype {}", path.display())).await?; + Err(anyhow::anyhow!("invalid command filename {}", path.display()).into()) + } } pub async fn handle_uri(sub: &str) -> crate::Result { - Ok(match sub.split_once('/') { - // ://pkg/{id} - installs package id - Some(("pkg", id)) => InternetPayload::InstallPackage { id: id.to_string() }, - // ://pack/{id} - installs pack id - Some(("pack", id)) => InternetPayload::InstallPack { id: id.to_string() }, - // unknown uri command: error - _ => { - send_message(&format!("invalid command {sub}")).await?; - return Err(anyhow::anyhow!("invalid command {sub}").into()); - } - }) + Ok(match sub.split_once('/') { + // ://pkg/{id} - installs package id + Some(("pkg", id)) => InternetPayload::InstallPackage { id: id.to_string() }, + // ://pack/{id} - installs pack id + Some(("pack", id)) => InternetPayload::InstallPack { id: id.to_string() }, + // unknown uri command: error + _ => { + send_message(&format!("invalid command {sub}")).await?; + return Err(anyhow::anyhow!("invalid command {sub}").into()); + } + }) } diff --git a/core/src/api/ingress.rs b/core/src/api/ingress.rs index 78736214..8ba5fd71 100644 --- a/core/src/api/ingress.rs +++ b/core/src/api/ingress.rs @@ -1,5 +1,5 @@ use crate::store::{IngressProcessType, IngressProcessor}; pub async fn check_ingress_feeds() -> crate::Result { - IngressProcessor::finished(IngressProcessType::IngressFeed).await + IngressProcessor::finished(IngressProcessType::IngressFeed).await } diff --git a/core/src/api/java.rs b/core/src/api/java.rs index b530ebce..0ea9c1ec 100644 --- a/core/src/api/java.rs +++ b/core/src/api/java.rs @@ -4,117 +4,137 @@ use reqwest::Method; use serde::Deserialize; use crate::proxy::send::{init_ingress, send_ingress}; -use crate::utils::{http::{fetch_advanced, fetch_json}, io::{self, IOError}, java::{self, get_java_version, JavaVersion}}; +use crate::utils::http::{fetch_advanced, fetch_json}; +use crate::utils::io::{self, IOError}; +use crate::utils::java::{self, get_java_version, JavaVersion}; use crate::State; -pub async fn filter_java_version( - java_version: Option, -) -> crate::Result> { - let java = java::locate_java().await?; - Ok(if let Some(java_version) = java_version { - java.into_iter().filter(|j| { - let jre_version = get_java_version(&j.version); - if let Ok(jre_version) = jre_version { - jre_version.1 == java_version - } else { - false - } - }).collect() - } else { - java - }) +pub async fn filter_java_version(java_version: Option) -> crate::Result> { + let java = java::locate_java().await?; + Ok(if let Some(java_version) = java_version { + java.into_iter() + .filter(|j| { + let jre_version = get_java_version(&j.version); + if let Ok(jre_version) = jre_version { + jre_version.1 == java_version + } else { + false + } + }) + .collect() + } else { + java + }) } #[onelauncher_debug::debugger] pub async fn install_java(java_version: u32) -> crate::Result { - let state = State::get().await?; + let state = State::get().await?; - let ingress = init_ingress( - crate::IngressType::DownloadJava { version: java_version }, - 100.0, "downloading java version").await?; + let ingress = init_ingress( + crate::IngressType::DownloadJava { + version: java_version, + }, + 100.0, + "downloading java version", + ) + .await?; - #[derive(Deserialize)] - struct Package { - pub download_url: String, - pub name: PathBuf, - } + #[derive(Deserialize)] + struct Package { + pub download_url: String, + pub name: PathBuf, + } - send_ingress(&ingress, 0.0, Some("fetching java api")).await?; - let packages = fetch_json::>(Method::GET, &format!( + send_ingress(&ingress, 0.0, Some("fetching java api")).await?; + let packages = fetch_json::>(Method::GET, &format!( "https://api.azul.com/metadata/v1/zulu/packages?arch={}&java_version={}&os={}&archive_type=zip&javafx_bundled=false&java_package_type=jre&page_size=1", std::env::consts::ARCH, java_version, std::env::consts::OS, ), None, None, &state.fetch_semaphore).await?; - send_ingress(&ingress, 10.0, Some("downloading java version")).await?; + send_ingress(&ingress, 10.0, Some("downloading java version")).await?; - if let Some(download) = packages.first() { - let file = fetch_advanced( - Method::GET, - &download.download_url, - None, - None, - None, - Some((&ingress, 80.0)), - &state.fetch_semaphore, - ).await?; + if let Some(download) = packages.first() { + let file = fetch_advanced( + Method::GET, + &download.download_url, + None, + None, + None, + Some((&ingress, 80.0)), + &state.fetch_semaphore, + ) + .await?; - let path = state.directories.java_dir().await; - let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file)) - .map_err(|err| IOError::from_zip(err))?; + let path = state.directories.java_dir().await; + let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file)) + .map_err(|err| IOError::from_zip(err))?; - if let Some(file) = archive.file_names().next() { - if let Some(dir) = file.split('/').next() { - let path = path.join(dir); - if path.exists() { io::remove_dir_all(path).await?; } - } - } + if let Some(file) = archive.file_names().next() { + if let Some(dir) = file.split('/').next() { + let path = path.join(dir); + if path.exists() { + io::remove_dir_all(path).await?; + } + } + } - send_ingress(&ingress, 0.0, Some("extracing java binary")).await?; - archive.extract(&path).map_err(|err| IOError::from_zip(err))?; - send_ingress(&ingress, 10.0, Some("extracted java binary")).await?; - let mut base_path = path.join(download.name.file_stem().unwrap_or_default().to_string_lossy().to_string()); + send_ingress(&ingress, 0.0, Some("extracing java binary")).await?; + archive + .extract(&path) + .map_err(|err| IOError::from_zip(err))?; + send_ingress(&ingress, 10.0, Some("extracted java binary")).await?; + let mut base_path = path.join( + download + .name + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(), + ); - #[cfg(target_os = "macos")] - { - base_path = base_path - .join(format!("zulu-{}.jre", java_version)) - .join("Contents") - .join("Home") - .join("bin") - .join("java") - } + #[cfg(target_os = "macos")] + { + base_path = base_path + .join(format!("zulu-{}.jre", java_version)) + .join("Contents") + .join("Home") + .join("bin") + .join("java") + } - #[cfg(not(target_os = "macos"))] - { - base_path = base_path.join("bin").join(java::JAVA_BIN) - } + #[cfg(not(target_os = "macos"))] + { + base_path = base_path.join("bin").join(java::JAVA_BIN) + } - Ok(base_path) - } else { - Err(anyhow::anyhow!( - "no java version found for version {}, os {}, and arch {}", - java_version, std::env::consts::OS, std::env::consts::ARCH, - ).into()) - } + Ok(base_path) + } else { + Err(anyhow::anyhow!( + "no java version found for version {}, os {}, and arch {}", + java_version, + std::env::consts::OS, + std::env::consts::ARCH, + ) + .into()) + } } pub async fn check_java(path: PathBuf) -> crate::Result> { - Ok(java::check_java_instance(&path).await) + Ok(java::check_java_instance(&path).await) } -pub async fn test_java( - path: PathBuf, - major: u32, -) -> crate::Result { - let jvm = match java::check_java_instance(&path).await { - Some(jvm) => jvm, - None => return Ok(false), - }; +pub async fn test_java(path: PathBuf, major: u32) -> crate::Result { + let jvm = match java::check_java_instance(&path).await { + Some(jvm) => jvm, + None => return Ok(false), + }; - let (maj, _) = get_java_version(&jvm.version)?; - Ok(maj == major) + let (maj, _) = get_java_version(&jvm.version)?; + Ok(maj == major) } pub async fn get_max_memory() -> crate::Result { - Ok(sys_info::mem_info() - .map_err(|_| anyhow::anyhow!("failed to get computer memory"))?.total) + Ok(sys_info::mem_info() + .map_err(|_| anyhow::anyhow!("failed to get computer memory"))? + .total) } diff --git a/core/src/api/logger.rs b/core/src/api/logger.rs index 90032c0c..26d9a441 100644 --- a/core/src/api/logger.rs +++ b/core/src/api/logger.rs @@ -1,198 +1,286 @@ //! OneLauncher log management -use std::io::Read; -use tokio::io::{AsyncReadExt, AsyncSeekExt}; -use serde::Serialize; -use futures::TryFutureExt; use crate::data::{Credentials, Directories, MinecraftCredentials}; use crate::prelude::ClusterPath; use crate::utils::io::{self, IOError}; +use futures::TryFutureExt; +use serde::Serialize; +use std::io::Read; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; // TODO: put this in the global store /// Core logging state and reader for OneLauncher. #[derive(Serialize, Debug)] pub struct LogManager { - /// The log file to read as a [`String`] - pub log_file: String, - /// The parsed and censored output of the logfile. - pub output: Option + /// The log file to read as a [`String`] + pub log_file: String, + /// The parsed and censored output of the logfile. + pub output: Option, } impl LogManager { - /// Initialize a new [`LogManager`]. - async fn initialize(cluster: &ClusterPath, log_file: String, clear: Option) -> crate::Result { - Ok(Self { - output: if clear.unwrap_or(false) { - None - } else { - Some(get_output_by_file(cluster, &log_file).await?) - }, - log_file, - }) - } + /// Initialize a new [`LogManager`]. + async fn initialize( + cluster: &ClusterPath, + log_file: String, + clear: Option, + ) -> crate::Result { + Ok(Self { + output: if clear.unwrap_or(false) { + None + } else { + Some(get_output_by_file(cluster, &log_file).await?) + }, + log_file, + }) + } } /// Get a [`Vec`] for a specific [`ClusterPath`]. #[tracing::instrument] -pub async fn get_logs(cluster_path: ClusterPath, clear: Option) -> crate::Result> { - let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { - c.cluster_path() - } else { - return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into()); - }; +pub async fn get_logs( + cluster_path: ClusterPath, + clear: Option, +) -> crate::Result> { + let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { + c.cluster_path() + } else { + return Err(anyhow::anyhow!( + "cluster {} isn't managed by onelauncher!", + cluster_path.to_string() + ) + .into()); + }; - let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; - let mut logs = Vec::new(); - if logs_folder.exists() { - for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? { - let entry: std::fs::DirEntry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?; - let path = entry.path(); - if !path.is_file() { continue; } - if let Some(name) = path.file_name() { - let name = name.to_string_lossy().to_string(); - logs.push(LogManager::initialize(&cluster_path, name, clear).await); - } - } - } + let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; + let mut logs = Vec::new(); + if logs_folder.exists() { + for entry in + std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? + { + let entry: std::fs::DirEntry = + entry.map_err(|e| IOError::with_path(e, &logs_folder))?; + let path = entry.path(); + if !path.is_file() { + continue; + } + if let Some(name) = path.file_name() { + let name = name.to_string_lossy().to_string(); + logs.push(LogManager::initialize(&cluster_path, name, clear).await); + } + } + } - let mut logs = logs.into_iter().collect::>>()?; - logs.sort_by_key(|x| x.log_file.clone()); - Ok(logs) + let mut logs = logs + .into_iter() + .collect::>>()?; + logs.sort_by_key(|x| x.log_file.clone()); + Ok(logs) } /// Delete all stored logs from a specific [`ClusterPath`]. #[tracing::instrument] pub async fn delete_logs(cluster_path: ClusterPath) -> crate::Result<()> { - let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { - c.cluster_path() - } else { - return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into()); - }; + let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { + c.cluster_path() + } else { + return Err(anyhow::anyhow!( + "cluster {} isn't managed by onelauncher!", + cluster_path.to_string() + ) + .into()); + }; - let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; - for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? { - let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?; - let path = entry.path(); - if path.is_dir() { io::remove_dir_all(&path).await? } - } + let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; + for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? { + let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?; + let path = entry.path(); + if path.is_dir() { + io::remove_dir_all(&path).await? + } + } - Ok(()) + Ok(()) } /// Get the [`LogManager`] for a specific [`ClusterPath`] log file. #[tracing::instrument] -pub async fn get_logs_by_file(cluster_path: ClusterPath, log_file: String) -> crate::Result { - let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { - c.cluster_path() - } else { - return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into()); - }; +pub async fn get_logs_by_file( + cluster_path: ClusterPath, + log_file: String, +) -> crate::Result { + let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { + c.cluster_path() + } else { + return Err(anyhow::anyhow!( + "cluster {} isn't managed by onelauncher!", + cluster_path.to_string() + ) + .into()); + }; - Ok(LogManager { - output: Some(get_output_by_file(&cluster_path, &log_file).await?), - log_file, - }) + Ok(LogManager { + output: Some(get_output_by_file(&cluster_path, &log_file).await?), + log_file, + }) } /// Get the default [`LogCursor`] for a [`ClusterPath`]. #[tracing::instrument] pub async fn get_log_cursor(cluster_path: ClusterPath, cursor: u64) -> crate::Result { - get_live_log_cursor(cluster_path, "latest.log", cursor).await + get_live_log_cursor(cluster_path, "latest.log", cursor).await } /// Get a live [`LogCursor`] for a [`ClusterPath`]'s log file. #[tracing::instrument] -pub async fn get_live_log_cursor(cluster_path: ClusterPath, log_file: &str, mut cursor: u64) -> crate::Result { - let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { - c.cluster_path() - } else { - return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into()); - }; +pub async fn get_live_log_cursor( + cluster_path: ClusterPath, + log_file: &str, + mut cursor: u64, +) -> crate::Result { + let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { + c.cluster_path() + } else { + return Err(anyhow::anyhow!( + "cluster {} isn't managed by onelauncher!", + cluster_path.to_string() + ) + .into()); + }; - let state = crate::State::get().await?; - let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; - let path = logs_folder.join(log_file); - if !path.exists() { return Ok(LogCursor { cursor: 0, new: false, output: LogOutput("".to_string()) }); } + let state = crate::State::get().await?; + let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; + let path = logs_folder.join(log_file); + if !path.exists() { + return Ok(LogCursor { + cursor: 0, + new: false, + output: LogOutput("".to_string()), + }); + } - let mut file = tokio::fs::File::open(&path).await.map_err(|e| IOError::with_path(e, &path))?; - let metadata = file.metadata().await.map_err(|e| IOError::with_path(e, &path))?; - let mut new = false; + let mut file = tokio::fs::File::open(&path) + .await + .map_err(|e| IOError::with_path(e, &path))?; + let metadata = file + .metadata() + .await + .map_err(|e| IOError::with_path(e, &path))?; + let mut new = false; - if cursor > metadata.len() { - cursor = 0; - new = true; - } + if cursor > metadata.len() { + cursor = 0; + new = true; + } - let mut buf = Vec::new(); - file.seek(std::io::SeekFrom::Start(cursor)).map_err(|e| IOError::with_path(e, &path)).await?; - let bytes_read = file.read_to_end(&mut buf).map_err(|e| IOError::with_path(e, &path)).await?; - let output = String::from_utf8_lossy(&buf).to_string(); - let cursor = cursor + bytes_read as u64; - let creds: Vec = state.users.read().await.users.clone().into_values().collect(); - let output = LogOutput::censor_secrets(output, &creds, None); - Ok(LogCursor { cursor, new, output }) + let mut buf = Vec::new(); + file.seek(std::io::SeekFrom::Start(cursor)) + .map_err(|e| IOError::with_path(e, &path)) + .await?; + let bytes_read = file + .read_to_end(&mut buf) + .map_err(|e| IOError::with_path(e, &path)) + .await?; + let output = String::from_utf8_lossy(&buf).to_string(); + let cursor = cursor + bytes_read as u64; + let creds: Vec = state + .users + .read() + .await + .users + .clone() + .into_values() + .collect(); + let output = LogOutput::censor_secrets(output, &creds, None); + Ok(LogCursor { + cursor, + new, + output, + }) } /// Delete a specific minecraft log file. #[tracing::instrument] pub async fn delete_logs_by_file(cluster_path: ClusterPath, log_file: &str) -> crate::Result<()> { - let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { - c.cluster_path() - } else { - return Err(anyhow::anyhow!("cluster {} isn't managed by onelauncher!", cluster_path.to_string()).into()); - }; + let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? { + c.cluster_path() + } else { + return Err(anyhow::anyhow!( + "cluster {} isn't managed by onelauncher!", + cluster_path.to_string() + ) + .into()); + }; - let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; - let path = logs_folder.join(log_file); - io::remove_dir_all(&path).await?; - Ok(()) + let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?; + let path = logs_folder.join(log_file); + io::remove_dir_all(&path).await?; + Ok(()) } /// Get a specific [`ClusterPath`] log file's [`LogOutput`]. #[tracing::instrument] -pub async fn get_output_by_file(cluster_path: &ClusterPath, log_file: &str) -> crate::Result { - let state = crate::State::get().await?; - let logs_folder = Directories::cluster_logs_dir(cluster_path).await?; - let path = logs_folder.join(log_file); - let credentials: Vec = state.users.read().await.users.clone().into_values().collect(); +pub async fn get_output_by_file( + cluster_path: &ClusterPath, + log_file: &str, +) -> crate::Result { + let state = crate::State::get().await?; + let logs_folder = Directories::cluster_logs_dir(cluster_path).await?; + let path = logs_folder.join(log_file); + let credentials: Vec = state + .users + .read() + .await + .users + .clone() + .into_values() + .collect(); - // todo: make this a utility function - if let Some(ext) = path.extension() { - let mut file = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?; - let mut contents = [0; 1024]; - let mut result = String::new(); + // todo: make this a utility function + if let Some(ext) = path.extension() { + let mut file = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?; + let mut contents = [0; 1024]; + let mut result = String::new(); - if ext == "gz" { - let mut gz = flate2::read::GzDecoder::new(std::io::BufReader::new(file)); + if ext == "gz" { + let mut gz = flate2::read::GzDecoder::new(std::io::BufReader::new(file)); - while gz.read(&mut contents).map_err(|e| IOError::with_path(e, &path))? > 0 { - result.push_str(&String::from_utf8_lossy(&contents)); - contents = [0; 1024]; - } + while gz + .read(&mut contents) + .map_err(|e| IOError::with_path(e, &path))? + > 0 + { + result.push_str(&String::from_utf8_lossy(&contents)); + contents = [0; 1024]; + } - return Ok(LogOutput::censor_secrets(result, &credentials, None)); - } else if ext == "log" { - while file.read(&mut contents).map_err(|e| IOError::with_path(e, &path))? > 0 { - result.push_str(&String::from_utf8_lossy(&contents)); - contents = [0; 1024]; - } + return Ok(LogOutput::censor_secrets(result, &credentials, None)); + } else if ext == "log" { + while file + .read(&mut contents) + .map_err(|e| IOError::with_path(e, &path))? + > 0 + { + result.push_str(&String::from_utf8_lossy(&contents)); + contents = [0; 1024]; + } - return Ok(LogOutput::censor_secrets(result, &credentials, None)); - } - } + return Ok(LogOutput::censor_secrets(result, &credentials, None)); + } + } - Err(anyhow::anyhow!("log file extension {} not supported", path.display()).into()) + Err(anyhow::anyhow!("log file extension {} not supported", path.display()).into()) } /// The log cursor used to parse logs passed into [`LogManager`]. #[derive(Serialize, Debug)] pub struct LogCursor { - /// The cursor ID. - pub cursor: u64, - /// The [`LogOutput`] associated with this cursor. - pub output: LogOutput, - /// Whether or not this corresponds is a new log file. - pub new: bool, + /// The cursor ID. + pub cursor: u64, + /// The [`LogOutput`] associated with this cursor. + pub output: LogOutput, + /// Whether or not this corresponds is a new log file. + pub new: bool, } /// The log output, a wrapper around [`String`], with utilities for parsing and censoring log contents. @@ -201,22 +289,26 @@ pub struct LogCursor { pub struct LogOutput(String); impl LogOutput { - /// Censor user secrets because sometimes mclogs misses them, including username, realname and minecraft credentials. - pub fn censor_secrets(mut output: String, credentials: &Vec, credentials_store: Option) -> Self { - let username = whoami::username(); - let realname = whoami::realname(); - output = output.replace(&format!("/{}/", username), "/{ENV_USERNAME}/"); - output = output.replace(&format!("\\{}\\", username), "\\{ENV_USERNAME}\\"); - output = output.replace(&format!("/{}/", realname), "/{ENV_REALNAME}/"); - output = output.replace(&format!("\\{}\\", realname), "\\{ENV_REALNAME}\\"); + /// Censor user secrets because sometimes mclogs misses them, including username, realname and minecraft credentials. + pub fn censor_secrets( + mut output: String, + credentials: &Vec, + credentials_store: Option, + ) -> Self { + let username = whoami::username(); + let realname = whoami::realname(); + output = output.replace(&format!("/{}/", username), "/{ENV_USERNAME}/"); + output = output.replace(&format!("\\{}\\", username), "\\{ENV_USERNAME}\\"); + output = output.replace(&format!("/{}/", realname), "/{ENV_REALNAME}/"); + output = output.replace(&format!("\\{}\\", realname), "\\{ENV_REALNAME}\\"); - for cred in credentials { - output = output.replace(&cred.access_token, "{MC_ACCESS_TOKEN}"); - output = output.replace(&cred.username, "{MC_USERNAME}"); - output = output.replace(&cred.id.as_simple().to_string(), "{MC_UUID}"); - output = output.replace(&cred.id.as_hyphenated().to_string(), "{MC_UUID}"); - } + for cred in credentials { + output = output.replace(&cred.access_token, "{MC_ACCESS_TOKEN}"); + output = output.replace(&cred.username, "{MC_USERNAME}"); + output = output.replace(&cred.id.as_simple().to_string(), "{MC_UUID}"); + output = output.replace(&cred.id.as_hyphenated().to_string(), "{MC_UUID}"); + } - Self(output) - } + Self(output) + } } diff --git a/core/src/api/metadata.rs b/core/src/api/metadata.rs index e39d5726..267c2d4e 100644 --- a/core/src/api/metadata.rs +++ b/core/src/api/metadata.rs @@ -7,55 +7,53 @@ pub use interpulse::api::modded::Manifest; /// Get a [`VersionManifest`] for all available Minecraft versions. #[tracing::instrument] pub async fn get_minecraft_versions() -> crate::Result { - let state = State::get().await?; - let meta = state.metadata.read().await.minecraft.clone(); + let state = State::get().await?; + let meta = state.metadata.read().await.minecraft.clone(); - Ok(meta) + Ok(meta) } /// Get a [`Manifest`] for all available Fabric versions. #[tracing::instrument] pub async fn get_fabric_versions() -> crate::Result { - let state = State::get().await?; - let meta = state.metadata.read().await.fabric.clone(); + let state = State::get().await?; + let meta = state.metadata.read().await.fabric.clone(); - Ok(meta) + Ok(meta) } /// Get a [`Manifest`] for all available Quilt versions. #[tracing::instrument] pub async fn get_quilt_versions() -> crate::Result { - let state = State::get().await?; - let meta = state.metadata.read().await.quilt.clone(); + let state = State::get().await?; + let meta = state.metadata.read().await.quilt.clone(); - Ok(meta) + Ok(meta) } /// Get a [`Manifest`] for all available Forge versions. #[tracing::instrument] pub async fn get_forge_versions() -> crate::Result { - let state = State::get().await?; - let meta = state.metadata.read().await.forge.clone(); + let state = State::get().await?; + let meta = state.metadata.read().await.forge.clone(); - Ok(meta) + Ok(meta) } /// Get a [`Manifest`] for all available NeoForge versions. #[tracing::instrument] pub async fn get_neoforge_versions() -> crate::Result { - let state = State::get().await?; - let meta = state.metadata.read().await.neoforge.clone(); + let state = State::get().await?; + let meta = state.metadata.read().await.neoforge.clone(); - Ok(meta) + Ok(meta) } /// Get a [`Manifest`] for all available Legacy Fabric versions. #[tracing::instrument] pub async fn get_legacy_fabric_versions() -> crate::Result { - let state = State::get().await?; - let meta = state.metadata.read().await.legacy_fabric.clone(); + let state = State::get().await?; + let meta = state.metadata.read().await.legacy_fabric.clone(); - Ok(meta) + Ok(meta) } - - diff --git a/core/src/api/minecraft.rs b/core/src/api/minecraft.rs index 14a951d9..1e68542d 100644 --- a/core/src/api/minecraft.rs +++ b/core/src/api/minecraft.rs @@ -5,73 +5,76 @@ use crate::store::{MinecraftCredentials, MinecraftLogin, State}; /// Begin a Microsoft authentication flow. #[tracing::instrument] pub async fn begin() -> crate::Result { - let state = State::get().await?; - let mut users = state.users.write().await; + let state = State::get().await?; + let mut users = state.users.write().await; - users.begin().await + users.begin().await } /// Complete a Microsoft authentication flow to recieve [`MinecraftCredentials`]. #[tracing::instrument] pub async fn finish(code: &str, flow: MinecraftLogin) -> crate::Result { - let state = State::get().await?; - let mut users = state.users.write().await; + let state = State::get().await?; + let mut users = state.users.write().await; - users.finish(code, flow).await + users.finish(code, flow).await } /// Get the current default user if it exists by [`uuid::Uuid`]. #[tracing::instrument] pub async fn get_default_user() -> crate::Result> { - let state = State::get().await?; - let users = state.users.read().await; + let state = State::get().await?; + let users = state.users.read().await; - Ok(users.default_user) + Ok(users.default_user) } /// Set the current default user by [`uuid::Uuid`]. #[tracing::instrument] pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> { - let user = get_user(user).await?; - let state = State::get().await?; - let mut users = state.users.write().await; + let user = get_user(user).await?; + let state = State::get().await?; + let mut users = state.users.write().await; - users.default_user = Some(user.id); - users.save().await?; + users.default_user = Some(user.id); + users.save().await?; - Ok(()) + Ok(()) } /// Remove a user account by its [`uuid::Uuid`] from the global store. #[tracing::instrument] pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> { - let state = State::get().await?; - let mut users = state.users.write().await; - - users.remove(user).await?; + let state = State::get().await?; + let mut users = state.users.write().await; - Ok(()) + users.remove(user).await?; + + Ok(()) } // TODO: Store this securely in stronghold /// Get a list of user [`MinecraftCredentials`]. #[tracing::instrument] pub async fn users() -> crate::Result> { - let state = State::get().await?; - let users = state.users.read().await; + let state = State::get().await?; + let users = state.users.read().await; - Ok(users.users.values().cloned().collect()) + Ok(users.users.values().cloned().collect()) } /// Get a specifc user's [`MinecraftCredentials`] by their [`uuid::Uuid`]. /// Use [`crate::store::MinecraftState#refresh`] instead. #[tracing::instrument] pub async fn get_user(user: uuid::Uuid) -> crate::Result { - let state = State::get().await?; - let users = state.users.read().await; + let state = State::get().await?; + let users = state.users.read().await; - let user = users.users.get(&user) - .ok_or_else(|| anyhow::anyhow!("failed to get nonexistent user with uuid {user}"))?.clone(); + let user = users + .users + .get(&user) + .ok_or_else(|| anyhow::anyhow!("failed to get nonexistent user with uuid {user}"))? + .clone(); - Ok(user) + Ok(user) } diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 2d8480d4..b21c9937 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -1,34 +1,33 @@ //! **OneLauncher API** -//! +//! //! API for interacting with our Rust core. -pub mod java; -pub mod proxy; -pub mod handler; -pub mod settings; -pub mod ingress; -pub mod processor; -pub mod minecraft; -pub mod metadata; -pub mod logger; -pub mod package; pub mod cluster; +pub mod handler; +pub mod ingress; +pub mod java; +pub mod logger; +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::{ - Credentials, MinecraftCredentials, Directories, InitHooks, JavaOptions, - PackageData, Memory, Loader, ClusterMeta, Settings, Theme, Resolution, - PackageType, ManagedDependency, ManagedPackage, ManagedUser, ManagedVersion, - }; + pub use crate::store::{ + ClusterMeta, Credentials, Directories, InitHooks, JavaOptions, Loader, ManagedDependency, + ManagedPackage, ManagedUser, ManagedVersion, Memory, MinecraftCredentials, PackageData, + PackageType, Resolution, Settings, Theme, + }; } pub mod prelude { - pub use crate::{ - data::*, - proxy::InternetPayload, - java, metadata, minecraft, package, processor, - settings, store::JavaVersions, store::ClusterPath, - utils::io::{canonicalize, IOError}, utils::java::JavaVersion, - State, - }; + 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::io::{canonicalize, IOError}; + pub use crate::utils::java::JavaVersion; + pub use crate::{java, metadata, minecraft, package, processor, settings, State}; } diff --git a/core/src/api/package/from.rs b/core/src/api/package/from.rs index e69de29b..4e51c6d3 100644 --- a/core/src/api/package/from.rs +++ b/core/src/api/package/from.rs @@ -0,0 +1,20 @@ +//! Install modpacks from different sources. + +use crate::data::{Loader, PackageData}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Modpack associated [`Cluster`] wrapper. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreatePackCluster { + pub name: String, + pub mc_version: String, + pub mod_loader: Loader, + pub loader_version: Option, + pub icon: Option, + pub icon_url: Option, + pub package_data: Option, + pub skip: Option, + pub skip_watch: Option, +} diff --git a/core/src/api/package/import/atlauncher.rs b/core/src/api/package/import/atlauncher.rs index e69de29b..8b137891 100644 --- a/core/src/api/package/import/atlauncher.rs +++ b/core/src/api/package/import/atlauncher.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/curseforge.rs b/core/src/api/package/import/curseforge.rs index e69de29b..8b137891 100644 --- a/core/src/api/package/import/curseforge.rs +++ b/core/src/api/package/import/curseforge.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/ftb.rs b/core/src/api/package/import/ftb.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/core/src/api/package/import/ftb.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/gdlauncher.rs b/core/src/api/package/import/gdlauncher.rs index e69de29b..8b137891 100644 --- a/core/src/api/package/import/gdlauncher.rs +++ b/core/src/api/package/import/gdlauncher.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/mod.rs b/core/src/api/package/import/mod.rs index 776d3866..1704cd5d 100644 --- a/core/src/api/package/import/mod.rs +++ b/core/src/api/package/import/mod.rs @@ -1,91 +1,281 @@ //! Manages importing data from other launchers. -use std::{fmt, path::{Path, PathBuf}}; -use serde::{Serialize, Deserialize}; -use crate::{prelude::ClusterPath, utils::io::{self, IOError}}; +use crate::prelude::ClusterPath; +use crate::proxy::send::{init_or_edit_ingress, send_ingress}; +use crate::proxy::IngressId; +use crate::store::Clusters; +use crate::utils::http::{self, IoSemaphore}; +use crate::utils::io::{self, IOError}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::path::{Path, PathBuf}; pub mod atlauncher; pub mod curseforge; +pub mod ftb; pub mod gdlauncher; +pub mod modrinth; pub mod multibased; +pub mod technic; +pub mod tlauncher; /// List of launcher types we support importing from. #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum ImportType { - /// MultiMC based launchers - MultiMC, - /// Prism Launcher has its own category because it has different logic (also is objectively better than mmc) - PrismLauncher, - /// GDLauncher - GDLauncher, - /// Curseforge's launcher - Curseforge, - /// ATLauncher - ATLauncher, - /// Modrinth app. - Modrinth, - /// Unknown import option (not widely adopted -> probably a custom launcher with a similar file structure to the above) - #[serde(other)] - Unknown, + /// MultiMC based launchers + MultiMC, + /// Prism Launcher has its own category because it has different logic (also is objectively better than mmc) + PrismLauncher, + /// GDLauncher + GDLauncher, + /// Curseforge's launcher + Curseforge, + /// ATLauncher + ATLauncher, + /// Modrinth app. + Modrinth, + /// TLauncher + TLauncher, + /// FTB Launcher + FTBLauncher, + /// Technic + Technic, + /// Unknown import option (not widely adopted -> probably a custom launcher with a similar file structure to the above) + #[serde(other)] + Unknown, } impl fmt::Display for ImportType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ImportType::MultiMC => write!(f, "MultiMC"), - ImportType::ATLauncher => write!(f, "ATLauncher"), - ImportType::Curseforge => write!(f, "Curseforge"), - ImportType::GDLauncher => write!(f, "GDLauncher"), - ImportType::Modrinth => write!(f, "Modrinth"), - ImportType::PrismLauncher => write!(f, "PrismLauncher"), - ImportType::Unknown => write!(f, "Unknown"), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ImportType::MultiMC => write!(f, "MultiMC"), + ImportType::ATLauncher => write!(f, "ATLauncher"), + ImportType::Curseforge => write!(f, "Curseforge"), + ImportType::GDLauncher => write!(f, "GDLauncher"), + ImportType::Modrinth => write!(f, "Modrinth"), + ImportType::PrismLauncher => write!(f, "PrismLauncher"), + ImportType::TLauncher => write!(f, "TLauncher"), + ImportType::FTBLauncher => write!(f, "FTB"), + ImportType::Technic => write!(f, "Technic"), + ImportType::Unknown => write!(f, "Unknown"), + } + } } pub async fn import_instances(import: ImportType, path: PathBuf) -> crate::Result> { - let instances_path = match import { - ImportType::GDLauncher | ImportType::ATLauncher => "instances".to_string(), - ImportType::Curseforge => "Instances".to_string(), - ImportType::MultiMC => multibased::get_instances_path(path.clone().join("multimc.cfg")).await.unwrap_or_else(|| "instances".to_string()), - ImportType::PrismLauncher => multibased::get_instances_path(path.clone().join("prismlauncher.cfg")).await.unwrap_or_else(|| "instances".to_string()), - ImportType::Modrinth => "Profiles".to_string(), - ImportType::Unknown | ImportType::Unknown => return Err(anyhow::anyhow!("launcher type unknown, cant import").into()), - }; + let instances_path = match import { + ImportType::GDLauncher | ImportType::ATLauncher => "instances".to_string(), + ImportType::Curseforge => "Instances".to_string(), + ImportType::MultiMC => multibased::get_instances_path(path.clone().join("multimc.cfg")) + .await + .unwrap_or_else(|| "instances".to_string()), + ImportType::PrismLauncher => { + multibased::get_instances_path(path.clone().join("prismlauncher.cfg")) + .await + .unwrap_or_else(|| "instances".to_string()) + } + ImportType::Modrinth => "profiles".to_string(), + ImportType::TLauncher => "instances".to_string(), + ImportType::Technic => "instances".to_string(), + ImportType::FTBLauncher => "instances".to_string(), + ImportType::Unknown | ImportType::Unknown => { + return Err(anyhow::anyhow!("launcher type unknown, cant import").into()) + } + }; - let instances_dir = path.join(&instances_path); - let mut instances = Vec::new(); - let mut dir = io::read_dir(&instances_dir).await.map_err(|_| { - anyhow::anyhow!("invalid {import} launcher path, failed to import.") - })?; + let instances_dir = path.join(&instances_path); + let mut instances = Vec::new(); + let mut dir = io::read_dir(&instances_dir) + .await + .map_err(|_| anyhow::anyhow!("invalid {import} launcher path, failed to import."))?; - while let Some(e) = dir.next_entry().await.map_err(|e| IOError::with_path(e, &instances_dir))? { - let path = e.path(); - if path.is_dir() { - if is_valid_instance(path.clone(), import).await { - let name = path.file_name(); - if let Some(name) = name { - instances.push(name.to_string_lossy().to_string()); - } - } - } - } + while let Some(e) = dir + .next_entry() + .await + .map_err(|e| IOError::with_path(e, &instances_dir))? + { + let path = e.path(); + if path.is_dir() { + if is_valid_instance(path.clone(), import).await { + let name = path.file_name(); + if let Some(name) = name { + instances.push(name.to_string_lossy().to_string()); + } + } + } + } - Ok(instances) + Ok(instances) } #[tracing::instrument] #[onelauncher_debug::debugger] pub async fn import_instance( - cluster_path: ClusterPath, - import: ImportType, - path: PathBuf, - instance_path: String, + cluster_path: ClusterPath, + import: ImportType, + path: PathBuf, + instance_path: String, ) -> crate::Result<()> { - tracing::debug!("importing instance from {instance_path}"); - - tracing::debug!("completed import of instance."); - Ok(()) + tracing::debug!("importing instance from {instance_path}"); + + let result = match import { + ImportType::MultiMC | ImportType::PrismLauncher => { + multibased::import(path, instance_path, cluster_path.clone()).await + } + ImportType::ATLauncher => { + atlauncher::import(path, instance_path, cluster_path.clone()).await + } + ImportType::GDLauncher => { + gdlauncher::import( + path.join("instances").join(instance_path), + cluster_path.clone(), + ) + .await + } + ImportType::Curseforge => { + curseforge::import( + path.join("Instances").join(instance_path), + cluster_path.clone(), + ) + .await + } + ImportType::Modrinth => { + modrinth::import( + path.join("profiles").join(instance_path), + cluster_path.clone(), + ) + .await + } + ImportType::TLauncher => { + tlauncher::import( + path.join("instances").join(instance_path), + cluster_path.clone(), + ) + .await + } + ImportType::Technic => { + technic::import( + path.join("instances").join(instance_path), + cluster_path.clone(), + ) + .await + } + ImportType::FTBLauncher => { + ftb::import( + path.join("instances").join(instance_path), + cluster_path.clone(), + ) + .await + } + ImportType::Unknown => { + return Err(anyhow::anyhow!("unknown launcher type").into()); + } + }; + + match result { + Ok(_) => {} + Err(e) => { + tracing::warn!("failed to import modpack: {:?}", e); + let _ = crate::api::cluster::remove(&cluster_path).await; + return Err(e); + } + } + + tokio::task::spawn(Clusters::update_versions()); + + tracing::debug!("completed import of instance."); + Ok(()) +} + +/// returns the default path for a given [`ImportType`]. +pub fn default_launcher_path(r#type: ImportType) -> Option { + let path = match r#type { + ImportType::MultiMC => None, + ImportType::PrismLauncher => Some(dirs::data_dir()?.join("PrismLauncher")), + ImportType::ATLauncher => Some(dirs::data_dir()?.join("ATLauncher")), + ImportType::GDLauncher => Some(dirs::data_dir()?.join("gdlauncher_next")), + ImportType::Curseforge => Some(dirs::home_dir()?.join("curseforge").join("minecraft")), + ImportType::Modrinth => Some(dirs::data_dir()?.join("theseus")), + ImportType::FTBLauncher => Some(dirs::data_dir()?.join("FTB")), + ImportType::Technic => Some(dirs::data_dir()?.join("Technic")), + ImportType::TLauncher => Some(dirs::data_dir()?.join("TLauncher")), + ImportType::Unknown => None, + }; + + let path = path?; + if path.exists() { + Some(path) + } else { + None + } +} + +/// checks if a [`PathBuf`] is a valid instance for a given [`ImportType`] +#[tracing::instrument] +#[onelauncher_debug::debugger] +pub async fn is_valid_instance(instance_path: PathBuf, r#type: ImportType) -> bool { + match r#type { + ImportType::MultiMC | ImportType::PrismLauncher => multibased::is_valid(insance_path).await, + ImportType::ATLauncher => atlauncher::is_valid(instance_path).await, + ImportType::GDLauncher => gdlauncher::is_valid(instance_path).await, + ImportType::Curseforge => curseforge::is_valid(instance_path).await, + ImportType::Modrinth => modrinth::is_valid(instance_path).await, + ImportType::TLauncher => tlauncher::is_valid(instance_path).await, + ImportType::FTBLauncher => ftb::is_valid(instance_path).await, + ImportType::Technic => technic::is_valid(instance_path).await, + ImportType::Unknown => false, + } +} + +/// caches an image file +#[tracing::instrument] +#[onelauncher_debug::debugger] +pub async fn cache_icon(icon_path: PathBuf) -> crate::Result> { + let state = crate::State::get().await?; + let bytes = tokio::fs::read(&icon_path).await; + if let Ok(bytes) = bytes { + let bytes = bytes::Bytes::from(bytes); + let cache_dir = &state.directories.caches_dir().await; + let semaphore = &state.io_semaphore; + Ok(Some( + http::write_icon(&icon_path.to_string_lossy(), cache_dir, bytes, semaphore).await?, + )) + } else { + Ok(None) + } +} + +pub async fn copy_minecraft( + cluster_path: ClusterPath, + minecraft_path: PathBuf, + io_semaphore: &IoSemaphore, + old_ingress: Option, +) -> crate::Result { + let cluster_full = cluster_path.full_path().await?; + let subfiles = sub(&minecraft_path).await?; + let total_subfiles = subfiles.len() as u64; + let ingress = init_or_edit_ingress( + old_ingress, + crate::IngressType::CopyCluster { + import: minecraft_path.clone(), + cluster_name: cluster_path.to_string(), + }, + total_subfiles as f64, + "copying files in preexisting cluster", + ) + .await?; + + for sub in subfiles { + let child = sub + .strip_prefix(&minecraft_path) + .map_err(|_| anyhow::anyhow!("invalid .minecraft file {}", &sub.display()))?; + let child = cluster_path.0.join(child); + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + http::copy(&sub, &child, io_semaphore).await?; + send_ingress(&ingress, 1.0, None).await?; + } + + Ok(ingress) } /// recursively get a [`Vec`] of all subfiles. @@ -93,13 +283,19 @@ pub async fn import_instance( #[async_recursion::async_recursion] #[tracing::instrument] pub async fn sub(path: &Path) -> crate::Result> { - if !path.is_dir() { return Ok(vec![path.to_path_buf()]); } - let mut files = Vec::new(); - let mut dir = io::read_dir(&path).await?; - while let Some(child) = dir.next_entry().await.map_err(|e| IOError::with_path(e, path))? { - let path_child = child.path(); - files.append(&mut sub(&path_child).await?); - } - - Ok(files) + if !path.is_dir() { + return Ok(vec![path.to_path_buf()]); + } + let mut files = Vec::new(); + let mut dir = io::read_dir(&path).await?; + while let Some(child) = dir + .next_entry() + .await + .map_err(|e| IOError::with_path(e, path))? + { + let path_child = child.path(); + files.append(&mut sub(&path_child).await?); + } + + Ok(files) } diff --git a/core/src/api/package/import/modrinth.rs b/core/src/api/package/import/modrinth.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/core/src/api/package/import/modrinth.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/multibased.rs b/core/src/api/package/import/multibased.rs index e69de29b..8b137891 100644 --- a/core/src/api/package/import/multibased.rs +++ b/core/src/api/package/import/multibased.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/technic.rs b/core/src/api/package/import/technic.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/core/src/api/package/import/technic.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/import/tlauncher.rs b/core/src/api/package/import/tlauncher.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/core/src/api/package/import/tlauncher.rs @@ -0,0 +1 @@ + diff --git a/core/src/api/package/mod.rs b/core/src/api/package/mod.rs index 2438e36b..79f529a7 100644 --- a/core/src/api/package/mod.rs +++ b/core/src/api/package/mod.rs @@ -1,5 +1,8 @@ -//! OneLauncher modpack management +//! **OneLauncher Package** +//! +//! Utilities for importing packages from and to OneLauncher. pub mod from; -pub mod mrpack; pub mod import; +pub mod mrpack; +pub mod zip; diff --git a/core/src/api/package/mrpack.rs b/core/src/api/package/mrpack.rs index e69de29b..4ad96dfe 100644 --- a/core/src/api/package/mrpack.rs +++ b/core/src/api/package/mrpack.rs @@ -0,0 +1 @@ +//! Import and Export `.mrpack` mod packs. diff --git a/core/src/api/package/zip.rs b/core/src/api/package/zip.rs index e69de29b..4f8a87a9 100644 --- a/core/src/api/package/zip.rs +++ b/core/src/api/package/zip.rs @@ -0,0 +1 @@ +//! Import and Export `.zip` mod packs diff --git a/core/src/api/processor.rs b/core/src/api/processor.rs index 32fe5da3..a2f86116 100644 --- a/core/src/api/processor.rs +++ b/core/src/api/processor.rs @@ -1,110 +1,107 @@ //! Process management interface +pub use crate::store::{Cluster, InitHooks, JavaOptions, Memory, Resolution, Settings, State}; +use crate::store::{ClusterPath, ProcessorChild}; use uuid::Uuid; -use crate::store::{ProcessorChild, ClusterPath}; -pub use crate::store::{InitHooks, JavaOptions, Memory, Cluster, Settings, Resolution, State}; /// check whether or not a process has completed by its [`Uuid`]. #[tracing::instrument] pub async fn uuid_is_finished(uuid: &Uuid) -> crate::Result { - Ok(uuid_exit_status(uuid).await?.is_some()) + Ok(uuid_exit_status(uuid).await?.is_some()) } /// check the exit status of a process by its [`Uuid`]. #[tracing::instrument] pub async fn uuid_exit_status(uuid: &Uuid) -> crate::Result> { - let state = State::get().await?; - let processor = state.processor.read().await; - processor.exit_status(uuid).await + let state = State::get().await?; + let processor = state.processor.read().await; + processor.exit_status(uuid).await } /// get all process [`Uuid`]s. #[tracing::instrument] pub async fn get_uuids() -> crate::Result> { - let state = State::get().await?; - let processor = state.processor.read().await; - Ok(processor.keys()) + let state = State::get().await?; + let processor = state.processor.read().await; + Ok(processor.keys()) } /// get all running process [`Uuid`]s. #[tracing::instrument] pub async fn get_running() -> crate::Result> { - let state = State::get().await?; - let processor = state.processor.read().await; - processor.running().await + let state = State::get().await?; + let processor = state.processor.read().await; + processor.running().await } /// get all running process' [`ClusterPath`]s. #[tracing::instrument] -pub async fn get_running_cluster_paths() -> crate::Result> -{ - let state = State::get().await?; - let processor = state.processor.read().await; - processor.running_cluster_paths().await +pub async fn get_running_cluster_paths() -> crate::Result> { + let state = State::get().await?; + let processor = state.processor.read().await; + processor.running_cluster_paths().await } /// get all running process' [`Cluster`]s. #[tracing::instrument] pub async fn get_running_clusters() -> crate::Result> { - let state = State::get().await?; - let processor = state.processor.read().await; - processor.running_clusters().await + let state = State::get().await?; + let processor = state.processor.read().await; + processor.running_clusters().await } /// get all processes by a [`ClusterPath`]. #[tracing::instrument] -pub async fn get_uuids_by_cluster_path( - cluster_path: ClusterPath, -) -> crate::Result> { - let state = State::get().await?; - let processor = state.processor.read().await; - processor.running_cluster(cluster_path).await +pub async fn get_uuids_by_cluster_path(cluster_path: ClusterPath) -> crate::Result> { + let state = State::get().await?; + let processor = state.processor.read().await; + processor.running_cluster(cluster_path).await } /// kill an existing and running process by its [`Uuid`] #[tracing::instrument] pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> { - let state = State::get().await?; - let processor = state.processor.read().await; + let state = State::get().await?; + let processor = state.processor.read().await; - if let Some(process) = processor.get(uuid) { - let mut process = process.write().await; - kill(&mut process).await - } else { - Ok(()) - } + if let Some(process) = processor.get(uuid) { + let mut process = process.write().await; + kill(&mut process).await + } else { + Ok(()) + } } /// wait for a child process to finish running its manager by its [`Uuid`]. #[tracing::instrument] pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> { - let state = State::get().await?; - let processor = state.processor.read().await; + let state = State::get().await?; + let processor = state.processor.read().await; - if let Some(process) = processor.get(uuid) { - let mut process = process.write().await; - wait_for(&mut process).await - } else { - Ok(()) - } + if let Some(process) = processor.get(uuid) { + let mut process = process.write().await; + wait_for(&mut process).await + } else { + Ok(()) + } } /// kill an existing and running process #[tracing::instrument(skip(process))] pub async fn kill(process: &mut ProcessorChild) -> crate::Result<()> { - process.current_child.write().await.kill().await?; - Ok(()) + process.current_child.write().await.kill().await?; + Ok(()) } /// wait for a child process to finish running its manager #[tracing::instrument(skip(process))] pub async fn wait_for(process: &mut ProcessorChild) -> crate::Result<()> { - process - .manager - .take() - .ok_or_else(||anyhow::anyhow!("manager already completed for process {}", process.uuid))? - .await? - .map_err(|err| anyhow::anyhow!("failed to run minecraft: {err}"))?; + process + .manager + .take() + .ok_or_else(|| anyhow::anyhow!("manager already completed for process {}", process.uuid))? + .await? + .map_err(|err| anyhow::anyhow!("failed to run minecraft: {err}"))?; - Ok(()) + Ok(()) } diff --git a/core/src/api/proxy/mod.rs b/core/src/api/proxy/mod.rs index b41ecdfc..15791554 100644 --- a/core/src/api/proxy/mod.rs +++ b/core/src/api/proxy/mod.rs @@ -1,8 +1,10 @@ //! 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 std::{collections::HashMap, path::PathBuf, sync::Arc}; use crate::store::{ClusterPath, IngressProcessor}; @@ -12,243 +14,240 @@ pub use utils::*; static PROXY_STATE: OnceCell> = OnceCell::const_new(); pub struct ProxyState { - #[cfg(feature = "tauri")] - pub app: tauri::AppHandle, - pub ingress_feeds: RwLock>, + #[cfg(feature = "tauri")] + pub app: tauri::AppHandle, + pub ingress_feeds: RwLock>, } #[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 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 { - 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(); - } + 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")] - { - let ingress_feed_uuid = ingress.ingress_uuid; - let event = ingress.ingress_type.clone(); - let fraction = ingress.current / ingress.total; + #[cfg(feature = "tauri")] + { + let ingress_feed_uuid = ingress.ingress_uuid; + let event = ingress.ingress_type.clone(); + let fraction = ingress.current / ingress.total; - use tauri::Manager; - let _ = proxy_state.app.emit("ingress", IngressPayload { - fraction: None, - message: "Complete".to_string(), - event, - ingress_uuid, - }); + use tauri::Manager; + 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); - } - } + tracing::trace!("exited at {fraction} for ingress: {:?}", ingress_uuid); + } + } - #[cfg(not(any(feature = "tauri", feature = "cli")))] - ingress_feeds.remove(&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; - } - }); - } + if crate::State::initalized() { + let _ = IngressProcessor::finish( + crate::store::IngressProcessType::IngressFeed, + ingress_uuid, + ) + .await; + } + }); + } } impl ProxyState { - #[cfg(feature = "tauri")] - pub async fn initialize(app: tauri::AppHandle) -> crate::Result> { - PROXY_STATE - .get_or_try_init(|| async { - Ok(Arc::new(Self { - app, - ingress_feeds: RwLock::new(HashMap::new()), - })) - }) - .await - .cloned() - } + #[cfg(feature = "tauri")] + pub async fn initialize(app: tauri::AppHandle) -> crate::Result> { + PROXY_STATE + .get_or_try_init(|| async { + Ok(Arc::new(Self { + app, + ingress_feeds: RwLock::new(HashMap::new()), + })) + }) + .await + .cloned() + } - #[cfg(not(feature = "tauri"))] - pub async fn initialize() -> crate::Result> { - PROXY_STATE - .get_or_try_init(|| async { - Ok(Arc::new(Self { - ingress_feeds: RwLock::new(HashMap::new()), - })) - }) - .await - .cloned() - } + #[cfg(not(feature = "tauri"))] + pub async fn initialize() -> crate::Result> { + 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 { - use tauri::Manager; - let value = Self::get().await?; - Ok(value.app.get_webview_window("main").unwrap()) - } + #[cfg(feature = "tauri")] + pub async fn get_main_window() -> crate::Result { + use tauri::Manager; + let value = Self::get().await?; + Ok(value.app.get_webview_window("main").unwrap()) + } - #[cfg(feature = "tauri")] - pub async fn get() -> crate::Result> { - Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone()) - } + #[cfg(feature = "tauri")] + pub async fn get() -> crate::Result> { + Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone()) + } - #[cfg(not(feature = "tauri"))] - pub async fn get() -> crate::Result> { - Self::initialize().await - } + #[cfg(not(feature = "tauri"))] + pub async fn get() -> crate::Result> { + Self::initialize().await + } - pub async fn list_ingress_feeds() -> crate::Result> { - let value = Self::get().await?; - let read = value.ingress_feeds.read().await; + pub async fn list_ingress_feeds() -> crate::Result> { + let value = Self::get().await?; + let read = value.ingress_feeds.read().await; - let mut display_list: HashMap = HashMap::new(); - for (uuid, ingress) in read.iter() { - display_list.insert(*uuid, ingress.clone()); - } + let mut display_list: HashMap = HashMap::new(); + for (uuid, ingress) in read.iter() { + display_list.insert(*uuid, ingress.clone()); + } - Ok(display_list) - } + Ok(display_list) + } } #[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, - }, - 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, - package_id: Option, - package_version: Option - } + Initialize, + DownloadJava { + version: u32, + }, + DownloadLoader { + cluster_path: PathBuf, + cluster_name: String, + }, + SyncCluster { + cluster_path: PathBuf, + cluster_name: String, + }, + 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, + package_id: Option, + package_version: Option, + }, } #[derive(Serialize, Clone)] pub struct IngressPayload { - pub event: IngressType, - pub ingress_uuid: Uuid, - pub fraction: Option, - pub message: String, + pub event: IngressType, + pub ingress_uuid: Uuid, + pub fraction: Option, + pub message: String, } #[derive(Serialize, Clone)] pub struct OfflinePayload { - pub offline: bool, + pub offline: bool, } #[derive(Serialize, Clone)] pub struct MessagePayload { - pub message: String, + pub message: String, } #[derive(Serialize, Clone)] #[serde(tag = "event")] pub enum InternetPayload { - InstallPackage { - id: String, - }, - InstallPack { - id: String, - }, - InstallPath { - path: PathBuf, - }, + InstallPackage { id: String }, + InstallPack { id: String }, + InstallPath { path: PathBuf }, } #[derive(Serialize, Clone)] pub struct ProcessPayload { - pub uuid: Uuid, - pub pid: u32, - pub event: ProcessPayloadType, - pub message: String, + pub uuid: Uuid, + pub pid: u32, + pub event: ProcessPayloadType, + pub message: String, } #[derive(Serialize, Clone, Debug)] #[serde(rename_all = "snake_case")] pub enum ProcessPayloadType { - Started, - Modified, - Finished, + Started, + Modified, + Finished, } #[derive(Serialize, Clone)] pub struct ClusterPayload { - pub uuid: Uuid, - pub cluster_path: ClusterPath, - pub path: PathBuf, - pub name: String, - pub event: ClusterPayloadType, + pub uuid: Uuid, + pub cluster_path: ClusterPath, + pub path: PathBuf, + pub name: String, + pub event: ClusterPayloadType, } #[derive(Serialize, Clone)] #[serde(rename_all = "snake_case")] pub enum ClusterPayloadType { - Created, - Inserted, - Synced, - Edited, - Deleted, + Created, + Inserted, + Synced, + Edited, + Deleted, } #[derive(Debug, thiserror::Error)] pub enum ProxyError { - #[error("event state was not initialized!")] - NotInitialized, + #[error("event state was not initialized!")] + NotInitialized, - #[error("non-existant ingress of key: {0}")] - NoIngressFound(Uuid), + #[error("non-existant ingress of key: {0}")] + NoIngressFound(Uuid), - #[cfg(feature = "tauri")] - #[error("Tauri error: {0}")] - TauriError(#[from] tauri::Error), + #[cfg(feature = "tauri")] + #[error("Tauri error: {0}")] + TauriError(#[from] tauri::Error), } diff --git a/core/src/api/proxy/send.rs b/core/src/api/proxy/send.rs index a800336b..4449fcd0 100644 --- a/core/src/api/proxy/send.rs +++ b/core/src/api/proxy/send.rs @@ -1,23 +1,25 @@ use super::IngressId; -use crate::api::proxy::{ProxyError, Ingress, IngressType, ProcessPayloadType, ClusterPayloadType, InternetPayload}; +use crate::api::proxy::{ + ClusterPayloadType, Ingress, IngressType, InternetPayload, ProcessPayloadType, ProxyError, +}; use crate::store::{ClusterPath, IngressProcessType, IngressProcessor}; use uuid::Uuid; #[cfg(feature = "tauri")] -use crate::api::proxy::{IngressPayload, ProcessPayload, ClusterPayload, MessagePayload}; +use crate::api::proxy::{ClusterPayload, IngressPayload, MessagePayload, ProcessPayload}; #[cfg(feature = "tauri")] use tauri::Manager; #[onelauncher_debug::debugger] pub async fn init_ingress( - ingress_type: IngressType, - total: f64, - title: &str, + ingress_type: IngressType, + total: f64, + title: &str, ) -> crate::Result { - let key = init_ingress_internal(ingress_type, total, title).await?; - IngressProcessor::add_ingress(IngressProcessType::IngressFeed, key.0).await?; - Ok(key) + let key = init_ingress_internal(ingress_type, total, title).await?; + IngressProcessor::add_ingress(IngressProcessType::IngressFeed, key.0).await?; + Ok(key) } #[cfg(feature = "cli")] @@ -25,183 +27,227 @@ const CLI_TOTAL_INGRESS: u64 = 1000; #[onelauncher_debug::debugger] pub async fn init_ingress_internal( - ingress_type: IngressType, - total: f64, - title: &str, + ingress_type: IngressType, + total: f64, + title: &str, ) -> crate::Result { - let proxy_state = crate::ProxyState::get().await?; - let key = IngressId(Uuid::new_v4()); + let proxy_state = crate::ProxyState::get().await?; + let key = IngressId(Uuid::new_v4()); - proxy_state.ingress_feeds.write().await.insert(key.0, Ingress { - ingress_uuid: key.0, - message: title.to_string(), - total, - current: 0.0, - last_sent: 0.0, - ingress_type, - #[cfg(feature = "cli")] - cli: { - let pb = indicatif::ProgressBar::new(CLI_TOTAL_INGRESS); - pb.set_position(0); - pb.set_style(indicatif::ProgressStyle::default_bar().template( + proxy_state.ingress_feeds.write().await.insert( + key.0, + Ingress { + ingress_uuid: key.0, + message: title.to_string(), + total, + current: 0.0, + last_sent: 0.0, + ingress_type, + #[cfg(feature = "cli")] + cli: { + let pb = indicatif::ProgressBar::new(CLI_TOTAL_INGRESS); + pb.set_position(0); + pb.set_style(indicatif::ProgressStyle::default_bar().template( "{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}" ).unwrap().progress_chars("#>-")); - pb - }, - }); + pb + }, + }, + ); - send_ingress(&key, 0.0, None).await?; - Ok(key) + send_ingress(&key, 0.0, None).await?; + Ok(key) } pub async fn init_or_edit_ingress( - id: Option, - ingress_type: IngressType, - total: f64, - title: &str, + id: Option, + ingress_type: IngressType, + total: f64, + title: &str, ) -> crate::Result { - if let Some(id) = id { - edit_ingress(&id, ingress_type, total, title).await?; + if let Some(id) = id { + edit_ingress(&id, ingress_type, total, title).await?; - Ok(id) - } else { - init_ingress(ingress_type, total, title).await - } + Ok(id) + } else { + init_ingress(ingress_type, total, title).await + } } pub async fn edit_ingress( - id: &IngressId, - ingress_type: IngressType, - total: f64, - title: &str, + id: &IngressId, + ingress_type: IngressType, + total: f64, + title: &str, ) -> crate::Result<()> { - let proxy_state = crate::ProxyState::get().await?; + let proxy_state = crate::ProxyState::get().await?; - if let Some(ingress) = proxy_state.ingress_feeds.write().await.get_mut(&id.0) { - ingress.ingress_type = ingress_type; - ingress.total = total; - ingress.message = title.to_string(); - ingress.current = 0.0; - ingress.last_sent = 0.0; - #[cfg(feature = "cli")] - { ingress.cli.reset(); } - }; + if let Some(ingress) = proxy_state.ingress_feeds.write().await.get_mut(&id.0) { + ingress.ingress_type = ingress_type; + ingress.total = total; + ingress.message = title.to_string(); + ingress.current = 0.0; + ingress.last_sent = 0.0; + #[cfg(feature = "cli")] + { + ingress.cli.reset(); + } + }; - send_ingress(id, 0.0, None).await?; - Ok(()) + send_ingress(id, 0.0, None).await?; + Ok(()) } #[allow(unused_variables)] #[tracing::instrument(level = "debug")] #[onelauncher_debug::debugger] pub async fn send_ingress( - key: &IngressId, - increment: f64, - message: Option<&str>, + key: &IngressId, + increment: f64, + message: Option<&str>, ) -> crate::Result<()> { - let proxy_state = crate::ProxyState::get().await?; - let mut ingress = proxy_state.ingress_feeds.write().await; - let ingress = match ingress.get_mut(&key.0) { - Some(f) => f, - None => { - return Err(ProxyError::NoIngressFound(key.0).into()); - } - }; + let proxy_state = crate::ProxyState::get().await?; + let mut ingress = proxy_state.ingress_feeds.write().await; + let ingress = match ingress.get_mut(&key.0) { + Some(f) => f, + None => { + return Err(ProxyError::NoIngressFound(key.0).into()); + } + }; - ingress.current += increment; - let display = ingress.current / ingress.total; - let display_conv = if display >= 1.0 { - None - } else { - Some(display) - }; + ingress.current += increment; + let display = ingress.current / ingress.total; + let display_conv = if display >= 1.0 { None } else { Some(display) }; - if f64::abs(display - ingress.last_sent) > 0.005 { - #[cfg(feature = "cli")] - { - ingress.cli.set_message(message.map(|x| x.to_string()).unwrap_or(ingress.message.clone())); - ingress.cli.set_position((display * CLI_TOTAL_INGRESS as f64).round() as u64); - } + if f64::abs(display - ingress.last_sent) > 0.005 { + #[cfg(feature = "cli")] + { + ingress.cli.set_message( + message + .map(|x| x.to_string()) + .unwrap_or(ingress.message.clone()), + ); + ingress + .cli + .set_position((display * CLI_TOTAL_INGRESS as f64).round() as u64); + } - #[cfg(feature = "tauri")] - proxy_state.app.emit("ingress", IngressPayload { - fraction: display_conv, - message: message.unwrap_or(&ingress.message).to_string(), - event: ingress.ingress_type.clone(), - ingress_uuid: ingress.ingress_uuid, - }).map_err(ProxyError::from)?; + #[cfg(feature = "tauri")] + proxy_state + .app + .emit( + "ingress", + IngressPayload { + fraction: display_conv, + message: message.unwrap_or(&ingress.message).to_string(), + event: ingress.ingress_type.clone(), + ingress_uuid: ingress.ingress_uuid, + }, + ) + .map_err(ProxyError::from)?; - ingress.last_sent = display; - } + ingress.last_sent = display; + } - Ok(()) + Ok(()) } pub async fn send_message(message: &str) -> crate::Result<()> { - #[cfg(feature = "tauri")] - { - let proxy_state = crate::ProxyState::get().await?; - proxy_state.app.emit("message", MessagePayload { message: message.to_string() }).map_err(ProxyError::from)?; - } + #[cfg(feature = "tauri")] + { + let proxy_state = crate::ProxyState::get().await?; + proxy_state + .app + .emit( + "message", + MessagePayload { + message: message.to_string(), + }, + ) + .map_err(ProxyError::from)?; + } - tracing::warn!("{}", message); - Ok(()) + tracing::warn!("{}", message); + Ok(()) } pub async fn send_offline(offline: bool) -> crate::Result<()> { - #[cfg(feature = "tauri")] - { - let proxy_state = crate::ProxyState::get().await?; - proxy_state.app.emit("offline", offline).map_err(ProxyError::from)?; - } + #[cfg(feature = "tauri")] + { + let proxy_state = crate::ProxyState::get().await?; + proxy_state + .app + .emit("offline", offline) + .map_err(ProxyError::from)?; + } - Ok(()) + Ok(()) } pub async fn send_internet(internet: InternetPayload) -> crate::Result<()> { - tracing::debug!("operation {}", serde_json::to_string(&internet)?); - #[cfg(feature = "tauri")] - { - let proxy_state = crate::ProxyState::get().await?; - proxy_state.app.emit("internet", internet).map_err(ProxyError::from)?; - } + tracing::debug!("operation {}", serde_json::to_string(&internet)?); + #[cfg(feature = "tauri")] + { + let proxy_state = crate::ProxyState::get().await?; + proxy_state + .app + .emit("internet", internet) + .map_err(ProxyError::from)?; + } - Ok(()) + Ok(()) } pub async fn send_process( - uuid: Uuid, - pid: u32, - event: ProcessPayloadType, - message: &str, + uuid: Uuid, + pid: u32, + event: ProcessPayloadType, + message: &str, ) -> crate::Result<()> { - #[cfg(feature = "tauri")] - { - let proxy_state = crate::ProxyState::get().await?; - proxy_state.app.emit("process", ProcessPayload { uuid, pid, event, message: message.to_string() }).map_err(ProxyError::from)?; - } + #[cfg(feature = "tauri")] + { + let proxy_state = crate::ProxyState::get().await?; + proxy_state + .app + .emit( + "process", + ProcessPayload { + uuid, + pid, + event, + message: message.to_string(), + }, + ) + .map_err(ProxyError::from)?; + } - Ok(()) + Ok(()) } pub async fn send_cluster( - uuid: Uuid, - cluster_path: &ClusterPath, - name: &str, - event: ClusterPayloadType, + uuid: Uuid, + cluster_path: &ClusterPath, + name: &str, + event: ClusterPayloadType, ) -> crate::Result<()> { - #[cfg(feature = "tauri")] - { - let path = cluster_path.full_path().await?; - let proxy_state = crate::ProxyState::get().await?; - proxy_state.app.emit("cluster", ClusterPayload { - uuid, - cluster_path: cluster_path.clone(), - path, - name: name.to_string(), - event, - }).map_err(ProxyError::from)?; - } + #[cfg(feature = "tauri")] + { + let path = cluster_path.full_path().await?; + let proxy_state = crate::ProxyState::get().await?; + proxy_state + .app + .emit( + "cluster", + ClusterPayload { + uuid, + cluster_path: cluster_path.clone(), + path, + name: name.to_string(), + event, + }, + ) + .map_err(ProxyError::from)?; + } - Ok(()) + Ok(()) } diff --git a/core/src/api/proxy/utils.rs b/core/src/api/proxy/utils.rs index db70fc2e..7463b8e4 100644 --- a/core/src/api/proxy/utils.rs +++ b/core/src/api/proxy/utils.rs @@ -47,31 +47,32 @@ macro_rules! ingress_join { #[tracing::instrument(skip(stream, f))] #[onelauncher_debug::debugger] pub async fn ingress_try_for_each( - stream: I, - limit: Option, - key: Option<&IngressId>, - total: f64, - num_futs: usize, - message: Option<&str>, - f: F, + stream: I, + limit: Option, + key: Option<&IngressId>, + total: f64, + num_futs: usize, + message: Option<&str>, + f: F, ) -> crate::Result<()> where - I: futures::TryStreamExt + TryStream, - F: FnMut(T) -> Fut + Send, - Fut: Future> + Send, - T: Send, + I: futures::TryStreamExt + TryStream, + F: FnMut(T) -> Fut + Send, + Fut: Future> + Send, + T: Send, { - let mut f = f; - stream.try_for_each_concurrent(limit, |item| { - let f = f(item); - async move { - f.await?; - if let Some(key) = key { - send_ingress(key, total / (num_futs as f64), message).await?; - } + let mut f = f; + stream + .try_for_each_concurrent(limit, |item| { + let f = f(item); + async move { + f.await?; + if let Some(key) = key { + send_ingress(key, total / (num_futs as f64), message).await?; + } - Ok(()) - } - }) - .await + Ok(()) + } + }) + .await } diff --git a/core/src/api/settings.rs b/core/src/api/settings.rs index 66c6e10b..c4d6b913 100644 --- a/core/src/api/settings.rs +++ b/core/src/api/settings.rs @@ -1,180 +1,201 @@ //! Settings Management -use std::path::{Path, PathBuf}; -use tokio::sync::RwLock; use crate::proxy::send::{init_ingress, send_ingress}; use crate::store::{self, Clusters, Directories, Settings}; use crate::State; +use std::path::{Path, PathBuf}; +use tokio::sync::RwLock; /// gets a [`Settings`] object state. #[tracing::instrument] pub async fn get() -> crate::Result { - let state = State::get().await?; - let settings = state.settings.read().await; - Ok(settings.clone()) + let state = State::get().await?; + let settings = state.settings.read().await; + Ok(settings.clone()) } /// sets a [`Settings`] object state. #[tracing::instrument] pub async fn set(settings: Settings) -> crate::Result<()> { - let state = State::get().await?; + let state = State::get().await?; - if settings.config_dir != state.settings.read().await.config_dir { - return Err(anyhow::anyhow!("ccannot change config directory as a setting").into()); - } + if settings.config_dir != state.settings.read().await.config_dir { + return Err(anyhow::anyhow!("ccannot change config directory as a setting").into()); + } - let (io, fetch) = async { - let read = state.settings.read().await; - ( - settings.max_async_io_operations != read.max_async_io_operations, - settings.max_async_fetches != read.max_async_fetches, - ) - }.await; + let (io, fetch) = async { + let read = state.settings.read().await; + ( + settings.max_async_io_operations != read.max_async_io_operations, + settings.max_async_fetches != read.max_async_fetches, + ) + } + .await; - let discord_rpc = { - let read = state.settings.read().await; - settings.disable_discord != read.disable_discord - }; + let discord_rpc = { + let read = state.settings.read().await; + settings.disable_discord != read.disable_discord + }; - { - *state.settings.write().await = settings; - } + { + *state.settings.write().await = settings; + } - if discord_rpc { - state.discord_rpc.clear(true).await?; - } + if discord_rpc { + state.discord_rpc.clear(true).await?; + } - if io { - state.reset_io_semaphore().await; - } + if io { + state.reset_io_semaphore().await; + } - if fetch { - state.reset_fetch_semaphore().await; - } + if fetch { + state.reset_fetch_semaphore().await; + } - State::sync().await?; - Ok(()) + State::sync().await?; + Ok(()) } /// sets the config and caches directory, this can have side effects. pub async fn set_directory(new: PathBuf) -> crate::Result<()> { - tracing::trace!("changing config directory to {}", new.display()); + tracing::trace!("changing config directory to {}", new.display()); - if !new.is_dir() { - return Err(anyhow::anyhow!("new config directory {} is not a folder!", new.display()).into()); - } + if !new.is_dir() { + return Err( + anyhow::anyhow!("new config directory {} is not a folder!", new.display()).into(), + ); + } - if !is_writable(new.clone()).await? { - return Err(anyhow::anyhow!("new config directory {} is not writeable", new.display()).into()); - } + if !is_writable(new.clone()).await? { + return Err( + anyhow::anyhow!("new config directory {} is not writeable", new.display()).into(), + ); + } - let ingress = init_ingress( - crate::IngressType::SyncConfig { new_path: new.clone() }, - 100.0, "syncing and changing configuration directory", - ).await?; + let ingress = init_ingress( + crate::IngressType::SyncConfig { + new_path: new.clone(), + }, + 100.0, + "syncing and changing configuration directory", + ) + .await?; - tracing::trace!("changing config directory, overriding state write access"); - let mut state_write = State::get_and_write().await?; - let old = state_write.directories.config_dir.read().await.clone(); + tracing::trace!("changing config directory, overriding state write access"); + let mut state_write = State::get_and_write().await?; + let old = state_write.directories.config_dir.read().await.clone(); - tracing::trace!("resetting file watcher after changing config directory"); - let fs_watcher = crate::utils::watcher::initialize_watcher().await?; - state_write.watcher = RwLock::new(fs_watcher); + tracing::trace!("resetting file watcher after changing config directory"); + let fs_watcher = crate::utils::watcher::initialize_watcher().await?; + state_write.watcher = RwLock::new(fs_watcher); - tracing::trace!("collecting all config files to be transfered"); - let mut cfg_entries = crate::utils::io::read_dir(&old).await?; - let across_filesystems = is_different_fs(&old, &new); - let mut entries = vec![]; - let mut cleanable = vec![]; + tracing::trace!("collecting all config files to be transfered"); + let mut cfg_entries = crate::utils::io::read_dir(&old).await?; + let across_filesystems = is_different_fs(&old, &new); + let mut entries = vec![]; + let mut cleanable = vec![]; - while let Some(e) = cfg_entries.next_entry().await.map_err(|err| crate::utils::io::IOError::with_path(err, &old))? { - let epath = e.path(); - if let Some(file_name) = epath.file_name() { - if file_name == store::CLUSTERS_FOLDER - || file_name == store::METADATA_FOLDER - { - if across_filesystems { - entries.extend(crate::package::import::sub(&epath).await?); - cleanable.push(epath.clone()); - } else { - entries.push(epath.clone()); - } - } - } - } + while let Some(e) = cfg_entries + .next_entry() + .await + .map_err(|err| crate::utils::io::IOError::with_path(err, &old))? + { + let epath = e.path(); + if let Some(file_name) = epath.file_name() { + if file_name == store::CLUSTERS_FOLDER || file_name == store::METADATA_FOLDER { + if across_filesystems { + entries.extend(crate::package::import::sub(&epath).await?); + cleanable.push(epath.clone()); + } else { + entries.push(epath.clone()); + } + } + } + } - tracing::trace!("transferring configuration files"); - let io_semaphore = &state_write.io_semaphore; - let nentries = entries.len() as f64; - for epath in entries { - let relpath = epath.strip_prefix(&old).map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?; - let newpath = new.join(relpath); - if across_filesystems { - crate::utils::http::copy(&epath, &newpath, io_semaphore).await?; - } else { - crate::utils::io::rename(epath.clone(), newpath.clone()).await?; - } + tracing::trace!("transferring configuration files"); + let io_semaphore = &state_write.io_semaphore; + let nentries = entries.len() as f64; + for epath in entries { + let relpath = epath + .strip_prefix(&old) + .map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?; + let newpath = new.join(relpath); + if across_filesystems { + crate::utils::http::copy(&epath, &newpath, io_semaphore).await?; + } else { + crate::utils::io::rename(epath.clone(), newpath.clone()).await?; + } - send_ingress(&ingress, 80.0 * (1.0 / nentries), None).await?; - } + send_ingress(&ingress, 80.0 * (1.0 / nentries), None).await?; + } - tracing::trace!("re-setting configuration settings"); - let settings = { - let mut settings = state_write.settings.write().await; - settings.config_dir = Some(new.clone()); + tracing::trace!("re-setting configuration settings"); + let settings = { + let mut settings = state_write.settings.write().await; + settings.config_dir = Some(new.clone()); - tracing::trace!("updating java paths and keys"); - for j in settings.java_versions.keys() { - if let Some(java) = settings.java_versions.get_mut(&j) { - if let Ok(relpath) = PathBuf::from(java.path.clone()).strip_prefix(&old) { - java.path = new.join(relpath).to_string_lossy().to_string(); - } - } - } + tracing::trace!("updating java paths and keys"); + for j in settings.java_versions.keys() { + if let Some(java) = settings.java_versions.get_mut(&j) { + if let Ok(relpath) = PathBuf::from(java.path.clone()).strip_prefix(&old) { + java.path = new.join(relpath).to_string_lossy().to_string(); + } + } + } - tracing::trace!("re-syncing settings file"); - settings.sync(&state_write.directories.settings_file()).await?; - settings.clone() - }; + tracing::trace!("re-syncing settings file"); + settings + .sync(&state_write.directories.settings_file()) + .await?; + settings.clone() + }; - tracing::trace!("re-initializing the config directory"); - state_write.directories = Directories::initalize(&settings)?; - let cleanable_nentries = cleanable.len(); - if cleanable_nentries > 0 { tracing::trace!("deleting old config files"); } - for ce in cleanable { - crate::utils::io::remove_dir_all(ce).await?; - send_ingress(&ingress, 10.0 * (1.0 / cleanable_nentries as f64), None).await?; - } + tracing::trace!("re-initializing the config directory"); + state_write.directories = Directories::initalize(&settings)?; + let cleanable_nentries = cleanable.len(); + if cleanable_nentries > 0 { + tracing::trace!("deleting old config files"); + } + for ce in cleanable { + crate::utils::io::remove_dir_all(ce).await?; + send_ingress(&ingress, 10.0 * (1.0 / cleanable_nentries as f64), None).await?; + } - tracing::trace!("re-resetting file watching system"); - let mut file_watcher = crate::utils::watcher::initialize_watcher().await?; - state_write.clusters = RwLock::new(Clusters::initialize(&state_write.directories, &mut file_watcher).await?); - state_write.watcher = RwLock::new(file_watcher); - send_ingress(&ingress, 10.0, None).await?; - - tracing::info!("successfully switched configuration directory to {}", new.display()); + tracing::trace!("re-resetting file watching system"); + let mut file_watcher = crate::utils::watcher::initialize_watcher().await?; + state_write.clusters = + RwLock::new(Clusters::initialize(&state_write.directories, &mut file_watcher).await?); + state_write.watcher = RwLock::new(file_watcher); + send_ingress(&ingress, 10.0, None).await?; - Ok(()) + tracing::info!( + "successfully switched configuration directory to {}", + new.display() + ); + + Ok(()) } /// checks if two paths are on different data storage devices fn is_different_fs(a: &Path, b: &Path) -> bool { - let roota = a.components().next(); - let rootb = b.components().next(); - roota != rootb + let roota = a.components().next(); + let rootb = b.components().next(); + roota != rootb } /// checks if a [`PathBuf`] is writable or locked. pub async fn is_writable(path: PathBuf) -> crate::Result { - let tmp = path.join(".tmp"); - match crate::utils::io::write(tmp.clone(), "test").await { - Ok(_) => { - crate::utils::io::remove_file(tmp).await?; - Ok(true) - } - Err(e) => { - tracing::error!("failed to write to config directory: {}", e); - Ok(false) - } - } + let tmp = path.join(".tmp"); + match crate::utils::io::write(tmp.clone(), "test").await { + Ok(_) => { + crate::utils::io::remove_file(tmp).await?; + Ok(true) + } + Err(e) => { + tracing::error!("failed to write to config directory: {}", e); + Ok(false) + } + } } diff --git a/core/src/constants.rs b/core/src/constants.rs index dd0abb2f..32830f0a 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -1,5 +1,5 @@ //! **OneLauncher Constants** -//! +//! //! Public constant variables and strings that are used throughout the launcher. //! Can be shared publically, any encrypted secrets should be stored in [`crate::store::credentials`]. diff --git a/core/src/error.rs b/core/src/error.rs index 53b78e8a..e41607dc 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,5 +1,5 @@ //! **OneLauncher Errors** -//! +//! //! Public wrappers around common OneLauncher errors. use tracing_error::InstrumentError; @@ -75,10 +75,18 @@ pub enum ErrorKind { #[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), @@ -97,7 +105,7 @@ pub enum ErrorKind { /// Wrapper around [`std::io::Error`] to handle non-wrapped std::io errors. #[error("I/O (std) error: {0}")] - StdIOError(#[from] std::io::Error), + StdIOError(#[from] std::io::Error), /// Wrapper around [`crate::utils::java::JavaError`] to handle java errors. #[error("there was an error managing java installations: {0}")] @@ -109,8 +117,8 @@ pub enum ErrorKind { /// Wrapper around [`tauri::Error`] to handle Tauri errors when the feature flag is enabled #[cfg(feature = "tauri")] - #[error("Tauri error: {0}")] - TauriError(#[from] tauri::Error), + #[error("Tauri error: {0}")] + TauriError(#[from] tauri::Error), } #[derive(Debug)] diff --git a/core/src/game/arguments.rs b/core/src/game/arguments.rs new file mode 100644 index 00000000..f33c800a --- /dev/null +++ b/core/src/game/arguments.rs @@ -0,0 +1,403 @@ +//! Programatic Minecraft argument parser which replaces variables in arguments + +use std::collections::{HashMap, HashSet}; +use std::io::prelude::BufRead; +use std::io::BufReader; +use std::path::Path; + +use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType}; +use interpulse::api::modded::SidedDataEntry; +use interpulse::utils::get_path_from_artifact; + +use crate::store::{Memory, MinecraftCredentials, Resolution}; +use crate::utils::io::IOError; +use crate::utils::platform::classpath_separator; + +const DUMMY_REPLACE_NEWLINE: &str = "\n"; + +#[allow(clippy::too_many_arguments)] +pub fn java_arguments( + arguments: Option<&[Argument]>, + natives_path: &Path, + libraries_path: &Path, + classpaths: &str, + version: &str, + memory: Memory, + custom_args: Vec, + java_arch: &str, +) -> crate::Result> { + let mut parsed = Vec::new(); + if let Some(args) = arguments { + parse_arguments( + args, + &mut parsed, + |a| { + parse_java_argument( + a.to_string(), + natives_path, + libraries_path, + classpaths, + version, + java_arch, + ) + }, + java_arch, + )?; + } else { + parsed.push(format!( + "-Djava.library.path={}", + dunce::canonicalize(natives_path) + .map_err(|_| anyhow::anyhow!( + "specified natives path {} not found", + natives_path.to_string_lossy() + ))? + .to_string_lossy() + )); + parsed.push("-cp".to_string()); + parsed.push(classpaths.to_string()); + } + + parsed.push(format!("-Xmx{}M", memory.maximum)); + parsed.push(format!("-Xms{}M", memory.minimum)); + + for arg in custom_args { + if !arg.is_empty() { + parsed.push(arg); + } + } + + Ok(parsed) +} + +#[allow(clippy::too_many_arguments)] +pub fn minecraft_arguments( + args: Option<&[Argument]>, + legacy_args: Option<&str>, + creds: &MinecraftCredentials, + version: &str, + asset_index: &str, + game_directory: &Path, + assets_directory: &Path, + version_type: &VersionType, + resolution: Resolution, + java_arch: &str, +) -> crate::Result> { + if let Some(args) = args { + let mut parsed = Vec::new(); + parse_arguments( + args, + &mut parsed, + |arg| { + parse_minecraft_argument( + arg, + &creds.access_token, + &creds.username, + &creds.id, + version, + asset_index, + game_directory, + assets_directory, + version_type, + resolution, + ) + }, + java_arch, + )?; + + Ok(parsed) + } else if let Some(legacy_args) = legacy_args { + let mut parsed = Vec::new(); + for arg in legacy_args.split(' ') { + parsed.push(parse_minecraft_argument( + &arg.replace(' ', DUMMY_REPLACE_NEWLINE), + &creds.access_token, + &creds.username, + &creds.id, + version, + asset_index, + game_directory, + assets_directory, + version_type, + resolution, + )?); + } + + Ok(parsed) + } else { + Ok(Vec::new()) + } +} + +pub fn processor_arguments>( + libraries_path: &Path, + args: &[T], + data: &HashMap, +) -> crate::Result> { + let mut parsed = Vec::new(); + + for arg in args { + let a = &arg.as_ref()[1..arg.as_ref().len() - 1]; + if arg.as_ref().starts_with('{') { + if let Some(entry) = data.get(a) { + parsed.push(if entry.client.starts_with('[') { + get_library( + libraries_path, + &entry.client[1..entry.client.len() - 1], + true, + )? + } else { + entry.client.clone() + }) + } + } else if arg.as_ref().starts_with('[') { + parsed.push(get_library(libraries_path, a, true)?) + } else { + parsed.push(arg.as_ref().to_string()) + } + } + + Ok(parsed) +} + +pub async fn main_class(path: String) -> crate::Result> { + let main_class = tokio::task::spawn_blocking(move || { + let zipfs = std::fs::File::open(&path).map_err(|e| IOError::with_path(e, &path))?; + let mut archive = zip::ZipArchive::new(zipfs) + .map_err(|_| anyhow::anyhow!("failed to read main class {}", path))?; + let file = archive + .by_name("META-INF/MANIFEST.MF") + .map_err(|_| anyhow::anyhow!("failed to read manifest {}", path))?; + + let zip_reader = BufReader::new(file); + for line in zip_reader.lines() { + let mut line = line.map_err(IOError::from)?; + line.retain(|c| !c.is_whitespace()); + + if line.starts_with("Main-Class:") { + if let Some(class) = line.split(':').nth(1) { + return Ok(Some(class.to_string())); + } + } + } + + Ok::, crate::Error>(None) + }) + .await??; + + Ok(main_class) +} + +#[allow(clippy::too_many_arguments)] +fn parse_minecraft_argument( + argument: &str, + access_token: &str, + username: &str, + uuid: &uuid::Uuid, + version: &str, + asset_index: &str, + game_directory: &Path, + assets_directory: &Path, + version_type: &VersionType, + resolution: Resolution, +) -> crate::Result { + Ok(argument + .replace("${accessToken}", access_token) + .replace("${auth_access_token}", access_token) + .replace("${auth_session}", access_token) + .replace("${auth_player_name}", username) + .replace("${auth_xuid}", "10") + .replace("${auth_uuid}", &uuid.simple().to_string()) + .replace("${client_id}", crate::constants::CLIENT_ID) + .replace("${user_properties}", "{}") + .replace("${user_type", "msa") + .replace("${version_name}", version) + .replace("${asset_index_name}", asset_index) + .replace( + "${game_directory}", + &dunce::canonicalize(game_directory) + .map_err(|_| { + anyhow::anyhow!( + "game directory {} doesn't exist", + game_directory.to_string_lossy() + ) + })? + .to_string_lossy(), + ) + .replace( + "${assets_root}", + &dunce::canonicalize(assets_directory) + .map_err(|_| { + anyhow::anyhow!( + "assets directory {} doesn't exist", + assets_directory.to_string_lossy() + ) + })? + .to_string_lossy(), + ) + .replace( + "${game_assets}", + &dunce::canonicalize(assets_directory) + .map_err(|_| { + anyhow::anyhow!( + "assets directory {} doesn't exist", + assets_directory.to_string_lossy() + ) + })? + .to_string_lossy(), + ) + .replace("${version_type}", version_type.as_str()) + .replace("${resolution_width}", &resolution.0.to_string()) + .replace("${resolution_height}", &resolution.1.to_string())) +} + +#[allow(clippy::too_many_arguments)] +fn parse_java_argument( + mut argument: String, + natives_path: &Path, + libraries_path: &Path, + classpaths: &str, + version: &str, + java_arch: &str, +) -> crate::Result { + argument.retain(|c| !c.is_whitespace()); + Ok(argument + .replace( + "${natives_directory}", + &dunce::canonicalize(natives_path) + .map_err(|_| { + anyhow::anyhow!( + "natives path {} doesn't exist", + natives_path.to_string_lossy() + ) + })? + .to_string_lossy(), + ) + .replace( + "${library_directory}", + &dunce::canonicalize(libraries_path) + .map_err(|_| { + anyhow::anyhow!( + "libraries path {} doesn't eixst", + libraries_path.to_string_lossy() + ) + })? + .to_string_lossy(), + ) + .replace("${classpath_seperator}", classpath_separator(java_arch)) + .replace("${launcher_name}", "onelauncher") + .replace("${launcher_version}", env!("CARGO_PKG_VERSION")) + .replace("${version_name}", version) + .replace("${classpath}", classpaths)) +} + +pub fn classpaths( + libraries_path: &Path, + libraries: &[Library], + client_path: &Path, + java_arch: &str, + updated: bool, +) -> crate::Result { + let mut classpaths = libraries + .iter() + .filter_map(|lib| { + if let Some(rules) = &lib.rules { + if !crate::game::rules(rules, java_arch, updated) { + return None; + } + } + + if !lib.include_in_classpath { + return None; + } + + Some(get_library(libraries_path, &lib.name, false)) + }) + .collect::, _>>()?; + + classpaths.insert( + dunce::canonicalize(client_path) + .map_err(|_| { + anyhow::anyhow!( + "specified classpath {} not found", + client_path.to_string_lossy() + ) + })? + .to_string_lossy() + .to_string(), + ); + + Ok(classpaths + .into_iter() + .collect::>() + .join(classpath_separator(java_arch))) +} + +pub fn get_classpath_library>( + libraries_path: &Path, + libraries: &[T], + java_arch: &str, +) -> crate::Result { + let classpaths = libraries + .iter() + .map(|lib| get_library(libraries_path, lib.as_ref(), false)) + .collect::, _>>()?; + + Ok(classpaths.join(classpath_separator(java_arch))) +} + +pub fn get_library( + libraries_path: &Path, + library: &str, + error_exist: bool, +) -> crate::Result { + let mut path = libraries_path.to_path_buf(); + path.push(get_path_from_artifact(library)?); + + if !path.exists() && error_exist { + return Ok(path.to_string_lossy().to_string()); + } + + let path = &dunce::canonicalize(&path) + .map_err(|_| anyhow::anyhow!("library file {} not found", path.to_string_lossy()))?; + + Ok(path.to_string_lossy().to_string()) +} + +fn parse_arguments( + args: &[Argument], + parsed: &mut Vec, + parse_function: ParseFn, + java_arch: &str, +) -> crate::Result<()> +where + ParseFn: Fn(&str) -> crate::Result, +{ + for arg in args { + match arg { + Argument::Normal(a) => { + let p = parse_function(&a.replace(' ', DUMMY_REPLACE_NEWLINE))?; + for split_p in p.split(DUMMY_REPLACE_NEWLINE) { + parsed.push(split_p.to_string()); + } + } + Argument::Ruled { rules, value } => { + if super::rules(rules, java_arch, true) { + match value { + ArgumentValue::Single(arg) => { + parsed.push(parse_function(&arg.replace(' ', DUMMY_REPLACE_NEWLINE))?); + } + ArgumentValue::Many(args) => { + for arg in args { + parsed.push(parse_function( + &arg.replace(' ', DUMMY_REPLACE_NEWLINE), + )?); + } + } + } + } + } + } + } + + Ok(()) +} diff --git a/core/src/game/metadata.rs b/core/src/game/metadata.rs new file mode 100644 index 00000000..cccf2f46 --- /dev/null +++ b/core/src/game/metadata.rs @@ -0,0 +1,328 @@ +//! Downloads Minecraft metadata to be launched + +use futures::prelude::*; +use interpulse as ip; +use tokio::sync::OnceCell; + +use interpulse::api::minecraft::{ + Asset, AssetsIndex, Library, Os, Version as GameVersion, VersionInfo as GameVersionInfo, +}; +use interpulse::api::modded::LoaderVersion; + +use crate::proxy::send::send_ingress; +use crate::proxy::utils::ingress_try_for_each; +use crate::proxy::IngressId; +use crate::store::State; +use crate::utils::http::*; +use crate::utils::io; +use crate::utils::platform::OsExt; + +#[tracing::instrument(skip(st, version))] +pub async fn download_minecraft( + st: &State, + version: &GameVersionInfo, + ingress: &IngressId, + java_arch: &str, + force: bool, + updated: bool, +) -> crate::Result<()> { + tracing::info!("downloading minecraft version {}", version.id); + let assets_index = download_assets_index(st, version, Some(ingress), force).await?; + let processors = if version + .processors + .as_ref() + .map(|p| !p.is_empty()) + .unwrap_or(false) + { + 25.0 + } else { + 40.0 + }; + + tokio::try_join! { + download_client(st, version, Some(ingress), force), + download_assets(st, version.assets == "legacy", &assets_index, Some(ingress), processors, force), + download_libraries(st, version.libraries.as_slice(), &version.id, Some(ingress), processors, java_arch, force, updated) + }?; + + tracing::info!("downloaded minecraft version {}", version.id); + Ok(()) +} + +#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))] +#[onelauncher_debug::debugger] +pub async fn download_version_info( + st: &State, + version: &GameVersion, + loader: Option<&LoaderVersion>, + force: Option, + ingress: Option<&IngressId>, +) -> crate::Result { + let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id)); + tracing::debug!("loading minecraft version info for minecraft version {version_id}"); + let path = st + .directories + .version_dir(&version_id) + .await + .join(format!("{version_id}.json")); + let result = if path.exists() && !force.unwrap_or(false) { + io::read(path) + .err_into::() + .await + .and_then(|ref it| Ok(serde_json::from_slice(it)?)) + } else { + tracing::info!( + "downloading minecraft version info for minecraft version {}", + &version_id + ); + let mut info = ip::api::minecraft::fetch_version_info(version).await?; + + if let Some(loader) = loader { + let partial = ip::api::modded::fetch_partial_version(&loader.url).await?; + info = ip::api::modded::merge_partial_version(partial, info); + } + + info.id = version_id.clone(); + + write(&path, &serde_json::to_vec(&info)?, &st.io_semaphore).await?; + Ok(info) + }?; + + if let Some(ingress) = ingress { + send_ingress(ingress, 5.0, None).await?; + } + + tracing::debug!("loaded minecraft version info for minecraft version {version_id}"); + Ok(result) +} + +#[tracing::instrument(skip_all)] +#[onelauncher_debug::debugger] +pub async fn download_assets_index( + st: &State, + version: &GameVersionInfo, + ingress: Option<&IngressId>, + force: bool, +) -> crate::Result { + tracing::debug!("loading assets index"); + let path = st + .directories + .index_dir() + .await + .join(format!("{}.json", &version.asset_index.id)); + + let result = if path.exists() && !force { + io::read(path) + .err_into::() + .await + .and_then(|ref it| Ok(serde_json::from_slice(it)?)) + } else { + let index = ip::api::minecraft::fetch_assets_index(version).await?; + write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?; + tracing::info!("downloaded assets index"); + Ok(index) + }?; + + if let Some(ingress) = ingress { + send_ingress(ingress, 5.0, None).await?; + } + + tracing::debug!("loaded assets index"); + Ok(result) +} + +#[tracing::instrument(skip(st, index))] +#[onelauncher_debug::debugger] +pub async fn download_assets( + st: &State, + legacy: bool, + index: &AssetsIndex, + ingress: Option<&IngressId>, + ingress_amount: f64, + force: bool, +) -> crate::Result<()> { + tracing::debug!("loading minecraft assets"); + let num_futs = index.objects.len(); + let assets = stream::iter(index.objects.iter()).map(Ok::<(&String, &Asset), crate::Error>); + + ingress_try_for_each( + assets, + None, + ingress, + ingress_amount, + num_futs, + None, + |(name, asset)| async move { + let hash = &asset.hash; + let resources = st.directories.object_dir(hash).await; + let url = format!( + "https://resources.download.minecraft.net/{sub_hash}/{hash}", + sub_hash = &hash[..2] + ); + let fetch_cell = OnceCell::::new(); + tokio::try_join! { + async { + if !resources.exists() || force { + let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?; + write(&resources, resource, &st.io_semaphore).await?; + tracing::trace!("fetched asset resource with hash {hash}"); + } + Ok::<_, crate::Error>(()) + }, + async { + let resources = st.directories.legacy_assets_dir().await.join( + name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) + ); + + if legacy && !resources.exists() || force { + let resource = fetch_cell.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)).await?; + write(&resources, resource, &st.io_semaphore).await?; + tracing::trace!("fetched legacy asset resource with hash {hash}"); + } + Ok::<_, crate::Error>(()) + }, + }?; + + tracing::trace!("loaded asset resource with hash {hash}"); + Ok(()) + }, + ) + .await?; + + tracing::debug!("loaded minecraft assets"); + Ok(()) +} + +#[tracing::instrument(skip(st, libraries))] +#[onelauncher_debug::debugger] +#[allow(clippy::too_many_arguments)] +pub async fn download_libraries( + st: &State, + libraries: &[Library], + version: &str, + ingress: Option<&IngressId>, + ingress_amount: f64, + java_arch: &str, + force: bool, + updated: bool, +) -> crate::Result<()> { + tracing::debug!("loading minecraft libraries"); + + tokio::try_join! { + io::create_dir_all(st.directories.libraries_dir().await), + io::create_dir_all(st.directories.version_natives_dir(version).await) + }?; + + let num_files = libraries.len(); + ingress_try_for_each( + stream::iter(libraries.iter()) + .map(Ok::<&Library, crate::Error>), + None, + ingress, + ingress_amount, + num_files, + None, + |lib| async move { + if let Some(rules) = &lib.rules { + if !super::rules(rules, java_arch, updated) { + tracing::trace!("skipped library {} due to rule", &lib.name); + return Ok(()); + } + } + + tokio::try_join! { + async { + let artifact_path = ip::utils::get_path_from_artifact(&lib.name)?; + let path = st.directories.libraries_dir().await.join(&artifact_path); + + match lib.downloads { + _ if path.exists() && !force => Ok(()), + Some(ip::api::minecraft::LibraryDownloads { + artifact: Some(ref artifact), + .. + }) => { + let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore).await?; + write(&path, &bytes, &st.io_semaphore).await?; + tracing::trace!("downloaded library {} into path {:?}", &lib.name, &path); + Ok::<_, crate::Error>(()) + } + _ => { + let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat(); + let bytes = fetch(&url, None, &st.fetch_semaphore).await?; + write(&path, &bytes, &st.io_semaphore).await?; + tracing::trace!("downloaded library {} into path {:?}", &lib.name, &path); + Ok::<_, crate::Error>(()) + } + } + }, + async { + if let Some((os_key, classifiers)) = None.or_else(|| Some(( + lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?, + lib.downloads.as_ref()?.classifiers.as_ref()? + ))) { + let parsed = os_key.replace("${arch}", crate::utils::platform::ARCH_WIDTH); + if let Some(native) = classifiers.get(&parsed) { + let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; + let reader = std::io::Cursor::new(&data); + if let Ok(mut archive) = zip::ZipArchive::new(reader) { + match archive.extract(st.directories.version_natives_dir(version).await) { + Ok(_) => tracing::debug!("downloaded native {}", &lib.name), + Err(err) => tracing::error!("failed to download native {}: {}", &lib.name, err) + } + } else { + tracing::error!("failed to extract native {}", &lib.name) + } + } + } + + Ok(()) + } + }?; + + tracing::debug!("loaded minecraft library {}", lib.name); + Ok(()) + } + ).await?; + + tracing::debug!("downloaded all minecraft libraries"); + Ok(()) +} + +#[tracing::instrument(skip_all)] +#[onelauncher_debug::debugger] +pub async fn download_client( + st: &State, + version: &GameVersionInfo, + ingress: Option<&IngressId>, + force: bool, +) -> crate::Result<()> { + let version_id = &version.id; + tracing::debug!("loading minecraft client for minecraft version {version_id}"); + + let client = version + .downloads + .get(&ip::api::minecraft::DownloadType::Client) + .ok_or(anyhow::anyhow!( + "no client downloads exist for {version_id}" + ))?; + + let path = st + .directories + .version_dir(version_id) + .await + .join(format!("{version_id}.jar")); + + if !path.exists() || force { + let result = fetch(&client.url, Some(&client.sha1), &st.fetch_semaphore).await?; + + write(&path, &result, &st.io_semaphore).await?; + tracing::trace!("fetched minecraft client version {version_id}"); + } + + if let Some(ingress) = ingress { + send_ingress(ingress, 9.0, None).await?; + } + + tracing::debug!("downloaded client for minecraft version {version_id}"); + Ok(()) +} diff --git a/core/src/game/mod.rs b/core/src/game/mod.rs new file mode 100644 index 00000000..b908d868 --- /dev/null +++ b/core/src/game/mod.rs @@ -0,0 +1,552 @@ +//! **OneLauncher Game** +//! +//! Manages, installs, and launches the core Minecraft game. + +use crate::prelude::JavaVersion; +use crate::processor; +use crate::proxy::send::{init_or_edit_ingress, send_ingress}; +use crate::proxy::{IngressId, IngressType}; +use crate::store::{ + self as st, Cluster, ClusterStage, MinecraftCredentials, ProcessorChild, State, +}; +use crate::utils::io::{self, IOError}; + +use chrono::Utc; +use interpulse as ip; +use interpulse::api::minecraft::{RuleAction, VersionInfo}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::process::Command; +use uuid::Uuid; + +mod arguments; +pub mod metadata; + +macro_rules! data_entry { + ($dest:expr; $($name:literal: client => $client:expr, server => $server:expr;)+) => { + $(std::collections::HashMap::insert( + $dest, + String::from($name), + interpulse::api::modded::SidedDataEntry { + client: String::from($client), + server: String::from($server), + }, + );)+ + } +} + +#[tracing::instrument(skip(cluster))] +#[onelauncher_debug::debugger] +pub async fn install_minecraft( + cluster: &Cluster, + old_ingress: Option, + repair: bool, +) -> crate::Result<()> { + let should_sync = old_ingress.is_some(); + let ingress = init_or_edit_ingress( + old_ingress, + IngressType::DownloadLoader { + cluster_name: cluster.meta.name.clone(), + cluster_path: cluster.get_full_path().await?, + }, + 100.0, + "downloading minecraft loader", + ) + .await?; + + crate::api::cluster::edit(&cluster.cluster_path(), |clus| { + clus.stage = ClusterStage::Downloading; + + async { Ok(()) } + }) + .await?; + + State::sync().await?; + + if should_sync { + Cluster::sync_packages(cluster.cluster_path(), true); + } + + let state = State::get().await?; + let instance_path = &io::canonicalize(cluster.get_full_path().await?)?; + let metadata = state.metadata.read().await; + + let version_idx = metadata + .minecraft + .versions + .iter() + .position(|g| g.id == cluster.meta.mc_version) + .ok_or(anyhow::anyhow!( + "invalid minecraft game version {}", + cluster.meta.mc_version + ))?; + + let version = &metadata.minecraft.versions[version_idx]; + let updated = version_idx + <= metadata + .minecraft + .versions + .iter() + .position(|g| g.id == "22w16a") + .unwrap_or(0); // LWJGL patching + let version_jar = cluster + .meta + .loader_version + .as_ref() + .map_or(version.id.clone(), |g| { + format!("{}-{}", version.id.clone(), g.id.clone()) + }); + let mut version_info = metadata::download_version_info( + &state, + version, + cluster.meta.loader_version.as_ref(), + Some(repair), + Some(&ingress), + ) + .await?; + // TODO: java checking and installation + let key = version_info + .java_version + .as_ref() + .map(|it| it.major_version) + .unwrap_or(8); + let (java_version, set_java) = + if let Some(java_version) = java_version_from_cluster(cluster, &version_info).await? { + (std::path::PathBuf::from(java_version.path), false) + } else { + (crate::api::java::install_java(key).await?, true) + }; + + let java_version = crate::api::java::check_java(java_version.clone()) + .await? + .ok_or_else(|| anyhow::anyhow!("java path validation failed: {:?}", java_version))?; + + if set_java { + { + let mut settings = state.settings.write().await; + settings + .java_versions + .insert(format!("JAVA_{key}"), java_version.clone()); + } + State::sync().await?; + } + + metadata::download_minecraft( + &state, + &version_info, + &ingress, + &java_version.arch, + repair, + updated, + ) + .await?; + + if let Some(processors) = &version_info.processors { + let client = state + .directories + .version_dir(&version_jar) + .await + .join(format!("{version_jar}.jar")); + let libraries = state.directories.libraries_dir().await; + if let Some(ref mut data) = version_info.data { + data_entry! { + data; + "SIDE": + client => "client", + server => ""; + "MINECRAFT_JAR": + client => client.to_string_lossy(), + server => ""; + "MINECRAFT_VERSION": + client => cluster.meta.mc_version.clone(), + server => ""; + "ROOT": + client => instance_path.to_string_lossy(), + server => ""; + "LIBRARY_DIR": + client => libraries.to_string_lossy(), + server => ""; + } + + send_ingress(&ingress, 0.0, Some("running forge processors")).await?; + let total_length = processors.len(); + for (index, processor) in processors.iter().enumerate() { + if let Some(sides) = &processor.sides { + if !sides.contains(&String::from("client")) { + continue; + } + } + + let cp = ref_owned!(cp = processor.classpath.clone() => { + cp.push(processor.jar.clone()) + }); + + let child = Command::new(&java_version.path) + .arg("-cp") + .arg(arguments::get_classpath_library( + &libraries, + &cp, + &java_version.arch, + )?) + .arg( + arguments::main_class(arguments::get_library( + &libraries, + &processor.jar, + false, + )?) + .await? + .ok_or_else(|| { + anyhow::anyhow!( + "failed to find processor main class for {}", + processor.jar + ) + })?, + ) + .args(arguments::processor_arguments( + &libraries, + &processor.args, + data, + )?) + .output() + .await + .map_err(|e| IOError::with_path(e, &java_version.path)) + .map_err(|err| anyhow::anyhow!("failed to run processor: {err}"))?; + + if !child.status.success() { + return Err(anyhow::anyhow!( + "error occured while running processor: {}", + String::from_utf8_lossy(&child.stderr) + ) + .into()); + } + + send_ingress( + &ingress, + 30.0 / total_length as f64, + Some(&format!( + "running forge processor {}/{}", + index, total_length + )), + ) + .await?; + } + } + } + + crate::api::cluster::edit(&cluster.cluster_path(), |clus| { + clus.stage = ClusterStage::Installed; + + async { Ok(()) } + }) + .await?; + State::sync().await?; + send_ingress(&ingress, 1.0, Some("installed minecraft successfully")).await?; + + Ok(()) +} + +#[tracing::instrument(skip_all)] +#[onelauncher_debug::debugger] +#[allow(clippy::too_many_arguments)] +pub async fn launch_minecraft( + cluster: &Cluster, + java_args: &[String], + env_args: &[(String, String)], + mc_options: &[(String, String)], + post_hook: Option, + credentials: &MinecraftCredentials, + resolution: &st::Resolution, + memory: &st::Memory, + wrapper: &Option, +) -> crate::Result>> { + if cluster.stage == ClusterStage::PackDownloading || cluster.stage == ClusterStage::Downloading + { + return Err(anyhow::anyhow!("cluster is still downloading").into()); + } + + if cluster.stage != ClusterStage::Installed { + install_minecraft(cluster, None, false).await?; + } + + let state = State::get().await?; + let metadata = state.metadata.read().await; + let instance_path = cluster.get_full_path().await?; + let instance_path = &io::canonicalize(instance_path)?; + + let version_index = metadata + .minecraft + .versions + .iter() + .position(|it| it.id == cluster.meta.mc_version) + .ok_or(anyhow::anyhow!( + "invalid game version {}", + cluster.meta.mc_version + ))?; + let version = &metadata.minecraft.versions[version_index]; + let updated = version_index + <= metadata + .minecraft + .versions + .iter() + .position(|x| x.id == "22w16a") + .unwrap_or(0); + let version_jar = cluster + .meta + .loader_version + .as_ref() + .map_or(version.id.clone(), |it| { + format!("{}-{}", version.id.clone(), it.id.clone()) + }); + let version_info = metadata::download_version_info( + &state, + version, + cluster.meta.loader_version.as_ref(), + None, + None, + ) + .await?; + let java_version = java_version_from_cluster(cluster, &version_info) + .await? + .ok_or_else(|| anyhow::anyhow!("missing java installation"))?; + let java_version = crate::api::java::check_java(java_version.path.clone().into()) + .await? + .ok_or_else(|| anyhow::anyhow!("java path invalid: {}", java_version.path))?; + let client_path = state + .directories + .version_dir(&version_jar) + .await + .join(format!("{version_jar}.jar")); + let args = version_info.arguments.clone().unwrap_or_default(); + let mut command = match wrapper { + Some(hook) => ref_owned!(it = Command::new(hook) => {it.arg(&java_version.path)}), + None => Command::new(&java_version.path), + }; + let env_args = Vec::from(env_args); + let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?; + if let Some(uuid) = existing.first() { + return Err(anyhow::anyhow!( + "cluster {} is already running ({uuid})", + cluster.cluster_path() + ) + .into()); + } + + command + .args( + arguments::java_arguments( + args.get(&ip::api::minecraft::ArgumentType::Jvm) + .map(|x| x.as_slice()), + &state.directories.version_natives_dir(&version_jar).await, + &state.directories.libraries_dir().await, + &arguments::classpaths( + &state.directories.libraries_dir().await, + version_info.libraries.as_slice(), + &client_path, + &java_version.arch, + updated, + )?, + &version_jar, + *memory, + Vec::from(java_args), + &java_version.arch, + )? + .into_iter() + .collect::>(), + ) + .arg(version_info.main_class.clone()) + .args( + arguments::minecraft_arguments( + args.get(&ip::api::minecraft::ArgumentType::Game) + .map(|a| a.as_slice()), + version_info.minecraft_arguments.as_deref(), + credentials, + &version.id, + &version_info.asset_index.id, + instance_path, + &state.directories.assets_dir().await, + &version.type_, + *resolution, + &java_version.arch, + )? + .into_iter() + .collect::>(), + ) + .current_dir(instance_path.clone()); + + // when cargo makes the DYLD_LIBRARY_PATH it breaks Minecraft + #[cfg(target_os = "macos")] + if std::env::var("CARGO").is_ok() { + command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); + } + + // remove preexisting Java options, as they should be set in the cluster settings. + command.env_remove("_JAVA_OPTIONS"); + command.envs(env_args); + + // overrides `options.txt` with our settings: i can't believe it's not yaml + use regex::Regex; + + if !mc_options.is_empty() { + let options_path = instance_path.join("options.txt"); + let mut options_string = String::new(); + if options_path.exists() { + options_string = io::read_to_string(&options_path).await?; + } + for (key, value) in mc_options { + let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?; + if !re.is_match(&options_string) { + options_string.push_str(&format!("\n{}:{}", key, value)); + } else { + let replaced_string = re + .replace_all(&options_string, &format!("{}:{}", key, value)) + .to_string(); + options_string = replaced_string; + } + } + + io::write(&options_path, options_string).await?; + } + + crate::api::cluster::edit(&cluster.cluster_path(), |clust| { + clust.meta.played_at = Some(Utc::now()); + + async { Ok(()) } + }) + .await?; + State::sync().await?; + + let mut censors = HashMap::new(); + let username = whoami::username(); + let realname = whoami::realname(); + censors.insert(format!("/{}/", username), "/{ENV_USERNAME}/".to_string()); + censors.insert( + format!("\\{}\\", username), + "\\{ENV_USERNAME}\\".to_string(), + ); + censors.insert(format!("/{}/", realname), "/{ENV_REALNAME}/".to_string()); + censors.insert( + format!("\\{}\\", realname), + "\\{ENV_REALNAME}\\".to_string(), + ); + censors.insert( + credentials.access_token.clone(), + "{MC_ACCESS_TOKEN}".to_string(), + ); + censors.insert(credentials.username.clone(), "{MC_USERNAME}".to_string()); + censors.insert( + credentials.id.as_simple().to_string(), + "{MC_UUID}".to_string(), + ); + censors.insert( + credentials.id.as_hyphenated().to_string(), + "{MC_UUID}".to_string(), + ); + + #[cfg(feature = "tauri")] + { + use crate::ProxyState; + let window = ProxyState::get_main_window().await?; + let settings = state.settings.read().await; + if settings.hide_on_launch { + window.minimize()?; + } + } + + if !*state.offline.read().await { + let _ = state + .discord_rpc + .set_activity(&format!("Playing {}", cluster.meta.name), true) + .await; + } + + let mut state_processor = state.processor.write().await; + state_processor + .insert_process( + Uuid::new_v4(), + cluster.cluster_path(), + command, + post_hook, + censors, + ) + .await +} + +#[tracing::instrument] +pub fn rules(rules: &[ip::api::minecraft::Rule], java_version: &str, updated: bool) -> bool { + let mut rule = rules + .iter() + .map(|r| rule(r, java_version, updated)) + .collect::>>(); + if rules + .iter() + .all(|r| matches!(r.action, RuleAction::Disallow)) + { + rule.push(Some(true)) + } + + !(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(|r| r.is_none())) +} + +#[tracing::instrument] +pub fn rule(rule: &ip::api::minecraft::Rule, java_version: &str, updated: bool) -> Option { + use ip::api::minecraft::{Rule, RuleAction}; + + let result = match rule { + Rule { + os: Some(ref os), .. + } => crate::utils::platform::os_rule(os, java_version, updated), + Rule { + features: Some(ref features), + .. + } => { + !features.is_demo_user.unwrap_or(true) + || features.has_custom_resolution.unwrap_or(false) + || !features.has_quick_plays_support.unwrap_or(true) + || !features.is_quick_play_multiplayer.unwrap_or(true) + || !features.is_quick_play_realms.unwrap_or(true) + || !features.is_quick_play_singleplayer.unwrap_or(true) + } + _ => return Some(true), + }; + + match rule.action { + RuleAction::Allow => { + if result { + Some(true) + } else { + None + } + } + RuleAction::Disallow => { + if result { + Some(false) + } else { + None + } + } + } +} + +pub async fn java_version_from_cluster( + cluster: &Cluster, + version_info: &VersionInfo, +) -> crate::Result> { + if let Some(java) = cluster.java.clone().and_then(|x| x.custom_version) { + Ok(Some(java)) + } else { + let key = version_info + .java_version + .as_ref() + .map(|x| x.major_version) + .unwrap_or(8); + + let state = State::get().await?; + let settings = state.settings.read().await; + + if let Some(j) = settings.java_versions.get(&format!("JAVA_{key}")) { + return Ok(Some(j.clone())); + } + + Ok(None) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 02b406a9..25014d6a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,5 +1,5 @@ //! # OneLauncher -//! +//! //! A library used as a core for our launcher and Rust APIs. #![warn(unused_import_braces, missing_debug_implementations)] @@ -15,9 +15,8 @@ pub mod game; pub mod logger; pub mod store; +pub use api::proxy::{Ingress, IngressType, ProxyState}; pub use api::*; pub use error::*; -pub use api::proxy::{ProxyState, Ingress, IngressType}; pub use logger::start_logger; -pub use store::InnerPathLinux; -pub use store::State; +pub use store::{InnerPathLinux, State}; diff --git a/core/src/logger.rs b/core/src/logger.rs index c4162b5b..1d742da0 100644 --- a/core/src/logger.rs +++ b/core/src/logger.rs @@ -1,16 +1,16 @@ //! **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; diff --git a/core/src/store/clusters.rs b/core/src/store/clusters.rs index 5dd72a1f..1a47a508 100644 --- a/core/src/store/clusters.rs +++ b/core/src/store/clusters.rs @@ -1,17 +1,19 @@ //! Core cluster state management -use std::{collections::HashMap, path::{Path, PathBuf}}; use chrono::{DateTime, Utc}; use futures::prelude::*; use interpulse::api::modded::LoaderVersion; use notify::RecommendedWatcher; use notify_debouncer_mini::Debouncer; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use uuid::Uuid; -use crate::proxy::{send::{send_cluster, send_message}, ClusterPayloadType}; -use crate::utils::{http::IoSemaphore, io::{self, IOError}}; -use crate::utils::http::write_icon; +use crate::proxy::send::{send_cluster, send_message}; +use crate::proxy::ClusterPayloadType; +use crate::utils::http::{write_icon, IoSemaphore}; +use crate::utils::io::{self, IOError}; use crate::utils::java::JavaVersion; use crate::State; @@ -27,15 +29,15 @@ pub(crate) struct Clusters(pub HashMap); #[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ClusterStage { - /// Installed with no downloads left. - Installed, - /// Downloading any sort of core metadata or mod file. - Downloading, - /// Downloading a full pack file, which is granted lower priority due to the high network demand. - PackDownloading, - /// Not installed at all. - #[default] - NotInstalled, + /// Installed with no downloads left. + Installed, + /// Downloading any sort of core metadata or mod file. + Downloading, + /// Downloading a full pack file, which is granted lower priority due to the high network demand. + PackDownloading, + /// Not installed at all. + #[default] + NotInstalled, } /// Relative Path wrapper to be used as an identifer for a cluster path. @@ -44,38 +46,46 @@ pub enum ClusterStage { pub struct ClusterPath(pub PathBuf); impl ClusterPath { - /// Get a new [`ClusterPath`] instance from an [`Into`]. - pub fn new(path: impl Into) -> Self { - ClusterPath(path.into()) - } + /// Get a new [`ClusterPath`] instance from an [`Into`]. + pub fn new(path: impl Into) -> Self { + ClusterPath(path.into()) + } - /// Get the full [`PathBuf`] of the current cluster path. - pub async fn full_path(&self) -> crate::Result { - let state = State::get().await?; - let clusters_dir = state.directories.clusters_dir().await; - Ok(clusters_dir.join(&self.0)) - } + /// Get the full [`PathBuf`] of the current cluster path. + pub async fn full_path(&self) -> crate::Result { + let state = State::get().await?; + let clusters_dir = state.directories.clusters_dir().await; + Ok(clusters_dir.join(&self.0)) + } - /// Validate the UTF of a cluster path. - pub fn validate(&self) -> crate::Result<&Self> { - self.0.to_str().ok_or(anyhow::anyhow!("invalid file path string {}!", self.0.clone().to_string_lossy())); - Ok(self) - } + /// Validate the UTF of a cluster path. + pub fn validate(&self) -> crate::Result<&Self> { + self.0.to_str().ok_or(anyhow::anyhow!( + "invalid file path string {}!", + self.0.clone().to_string_lossy() + )); + Ok(self) + } - /// Create a [`ClusterPath`] from a full [`PathBuf`]. - pub async fn from_path(path: PathBuf) -> crate::Result { - let path: PathBuf = io::canonicalize(path)?; - let clusters_dir = io::canonicalize(State::get().await?.directories.clusters_dir().await)?; - path.strip_prefix(clusters_dir).ok().and_then(|f| f.file_name()).ok_or_else(|| anyhow::anyhow!("path {} is not a cluster path", path.to_string_lossy()))?; + /// Create a [`ClusterPath`] from a full [`PathBuf`]. + pub async fn from_path(path: PathBuf) -> crate::Result { + let path: PathBuf = io::canonicalize(path)?; + let clusters_dir = io::canonicalize(State::get().await?.directories.clusters_dir().await)?; + path.strip_prefix(clusters_dir) + .ok() + .and_then(|f| f.file_name()) + .ok_or_else(|| { + anyhow::anyhow!("path {} is not a cluster path", path.to_string_lossy()) + })?; - Ok(Self(path)) - } + Ok(Self(path)) + } } impl std::fmt::Display for ClusterPath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.display().fmt(f) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.display().fmt(f) + } } /// Used for backwards compatibility for modpacks which handle windows paths strangely. @@ -84,26 +94,26 @@ impl std::fmt::Display for ClusterPath { pub struct InnerPathLinux(pub String); impl InnerPathLinux { - /// Get the first 2 components of the inner path. - pub fn get_components(&self) -> String { - self.to_string() - .split('/') - .take(2) - .collect::>() - .join("/") - } + /// Get the first 2 components of the inner path. + pub fn get_components(&self) -> String { + self.to_string() + .split('/') + .take(2) + .collect::>() + .join("/") + } } impl std::fmt::Display for InnerPathLinux { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } } impl From for InnerPathLinux { - fn from(value: RawPackagePath) -> Self { - InnerPathLinux(value.0.replace('\\', "/")) - } + fn from(value: RawPackagePath) -> Self { + InnerPathLinux(value.0.replace('\\', "/")) + } } /// Used for backwards compatibility for modpacks which handle windows paths strangely. @@ -112,9 +122,9 @@ impl From for InnerPathLinux { struct RawPackagePath(pub String); impl From for RawPackagePath { - fn from(value: InnerPathLinux) -> Self { - RawPackagePath(value.0) - } + fn from(value: InnerPathLinux) -> Self { + RawPackagePath(value.0) + } } /// Relative [`PathBuf`] for a specific [`Package`] of a [`Cluster`]. @@ -123,575 +133,630 @@ impl From for RawPackagePath { pub struct PackagePath(pub PathBuf); impl PackagePath { - /// Transform a full [`PathBuf`] into a relative [`PackagePath`]. - pub async fn from_path(path: &PathBuf) -> crate::Result { - let clusters_dir: PathBuf = std::fs::canonicalize( - State::get().await?.directories.clusters_dir().await - )?; - let path: PathBuf = std::fs::canonicalize(path)?; - let path = path - .strip_prefix(clusters_dir) - .ok() - .map(|p| p.components().skip(1).collect::()) - .ok_or_else(|| anyhow::anyhow!("path {path:?} does not exist in a cluster!", path = path))?; + /// Transform a full [`PathBuf`] into a relative [`PackagePath`]. + pub async fn from_path(path: &PathBuf) -> crate::Result { + let clusters_dir: PathBuf = + std::fs::canonicalize(State::get().await?.directories.clusters_dir().await)?; + let path: PathBuf = std::fs::canonicalize(path)?; + let path = path + .strip_prefix(clusters_dir) + .ok() + .map(|p| p.components().skip(1).collect::()) + .ok_or_else(|| { + anyhow::anyhow!("path {path:?} does not exist in a cluster!", path = path) + })?; - Ok(Self(path)) - } + Ok(Self(path)) + } - /// Get the full [`PathBuf`] of the current package path. - pub async fn full_path(&self, cluster: ClusterPath) -> crate::Result { - let cluster_dir = cluster.full_path().await?; - Ok(cluster_dir.join(&self.0)) - } + /// Get the full [`PathBuf`] of the current package path. + pub async fn full_path(&self, cluster: ClusterPath) -> crate::Result { + let cluster_dir = cluster.full_path().await?; + Ok(cluster_dir.join(&self.0)) + } - /// Get the [`InnerPathLinux`] of a [`PackagePath`]. - pub fn inner_path(&self) -> InnerPathLinux { - InnerPathLinux(self.0.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::>().join("/")) - } + /// Get the [`InnerPathLinux`] of a [`PackagePath`]. + pub fn inner_path(&self) -> InnerPathLinux { + InnerPathLinux( + self.0 + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/"), + ) + } - /// Create a new PackagePath from a relative path. - pub fn new(path: &Path) -> Self { - PackagePath(PathBuf::from(path)) - } + /// Create a new PackagePath from a relative path. + pub fn new(path: &Path) -> Self { + PackagePath(PathBuf::from(path)) + } } /// Represents a single Instance and installation of Minecraft /// Contains settings and identifiers on a per-Cluster basis, falling back to default settings for Options<> #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Cluster { - /// The Uuid of a specific cluster. - pub uuid: Uuid, - /// The download stage that of a cluster. - #[serde(default)] - pub stage: ClusterStage, - /// The core path of the cluster. - #[serde(default)] - pub path: PathBuf, - /// The associated cluster metadata. - pub meta: ClusterMeta, - /// The per-cluster JVM memory allocation options. - #[serde(skip_serializing_if = "Option::is_none")] - pub memory: Option, - /// The per-cluster JVM options. - #[serde(skip_serializing_if = "Option::is_none")] - pub java: Option, - /// The per-cluster Minecraft window default resolution. - #[serde(skip_serializing_if = "Option::is_none")] - pub resolution: Option, - /// The per-cluster Minecraft window fullscreen status. - pub force_fullscreen: Option, - // The per-cluster initialization hooks. - #[serde(skip_serializing_if = "Option::is_none")] - pub init_hooks: Option, - pub packages: HashMap, - #[serde(default)] - pub update: Option, + /// The Uuid of a specific cluster. + pub uuid: Uuid, + /// The download stage that of a cluster. + #[serde(default)] + pub stage: ClusterStage, + /// The core path of the cluster. + #[serde(default)] + pub path: PathBuf, + /// The associated cluster metadata. + pub meta: ClusterMeta, + /// The per-cluster JVM memory allocation options. + #[serde(skip_serializing_if = "Option::is_none")] + pub memory: Option, + /// The per-cluster JVM options. + #[serde(skip_serializing_if = "Option::is_none")] + pub java: Option, + /// The per-cluster Minecraft window default resolution. + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + /// The per-cluster Minecraft window fullscreen status. + pub force_fullscreen: Option, + // The per-cluster initialization hooks. + #[serde(skip_serializing_if = "Option::is_none")] + pub init_hooks: Option, + pub packages: HashMap, + #[serde(default)] + pub update: Option, } /// Represents core Cluster metadata ([`Cluster#meta`]). #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ClusterMeta { - /// The user-facing name of the cluster stored as a string. - pub name: String, + /// The user-facing name of the cluster stored as a string. + pub name: String, - /// The user-facing group of the cluster stored as a string. - #[serde(default)] - pub group: Vec, - /// The associated Minecraft version of the cluster as last updated. - pub mc_version: String, + /// The user-facing group of the cluster stored as a string. + #[serde(default)] + pub group: Vec, + /// The associated Minecraft version of the cluster as last updated. + pub mc_version: String, - /// The associated mod [`Loader`] as specified in the cluster. - #[serde(default)] - pub loader: Loader, - /// The associated mod [`LoaderVersion`] if available. - #[serde(skip_serializing_if = "Option::is_none")] - pub loader_version: Option, + /// The associated mod [`Loader`] as specified in the cluster. + #[serde(default)] + pub loader: Loader, + /// The associated mod [`LoaderVersion`] if available. + #[serde(skip_serializing_if = "Option::is_none")] + pub loader_version: Option, - /// The time that the cluster was created in [`DateTime`]. - #[serde(default)] - pub created_at: DateTime, - /// The time the cluster was last modified in [`DateTime`]. - #[serde(default)] - pub modified_at: DateTime, - /// The last time the cluster was played in [`DateTime`]. - /// (Defaults to None if the cluster has never been played) - #[serde(skip_serializing_if = "Option::is_none")] - pub played_at: Option>, - /// The overall time played stored as a [`u64`]. - #[serde(default)] - pub overall_played: u64, - /// The recent time played stored as a [`u64`]. - #[serde(default)] - pub recently_played: u64, + /// The time that the cluster was created in [`DateTime`]. + #[serde(default)] + pub created_at: DateTime, + /// The time the cluster was last modified in [`DateTime`]. + #[serde(default)] + pub modified_at: DateTime, + /// The last time the cluster was played in [`DateTime`]. + /// (Defaults to None if the cluster has never been played) + #[serde(skip_serializing_if = "Option::is_none")] + pub played_at: Option>, + /// The overall time played stored as a [`u64`]. + #[serde(default)] + pub overall_played: u64, + /// The recent time played stored as a [`u64`]. + #[serde(default)] + pub recently_played: u64, - /// The associated [`PackageData`] and modpack data for the cluster. - #[serde(skip_serializing_if = "Option::is_none")] - pub package_data: Option, + /// The associated [`PackageData`] and modpack data for the cluster. + #[serde(skip_serializing_if = "Option::is_none")] + pub package_data: Option, - /// The user-facing cluster icon as a [`PathBuf`]. - #[serde(skip_serializing_if = "Option::is_none")] - pub icon: Option, - /// The user-facing cluster icon as a URL string. - #[serde(skip_serializing_if = "Option::is_none")] - pub icon_url: Option + /// The user-facing cluster icon as a [`PathBuf`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + /// The user-facing cluster icon as a URL string. + #[serde(skip_serializing_if = "Option::is_none")] + pub icon_url: Option, } /// Optional data used to link a specific cluster to a package project. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct PackageData { - /// The package ID as a String. - pub package_id: Option, - /// The version of the package as a String. - pub version_id: Option, - /// Whether or not the current package is locked (for legacy modpack support). - #[serde(default = "default_locked")] - pub locked: Option, + /// The package ID as a String. + pub package_id: Option, + /// The version of the package as a String. + pub version_id: Option, + /// Whether or not the current package is locked (for legacy modpack support). + #[serde(default = "default_locked")] + pub locked: Option, } -pub fn default_locked() -> Option { Some(true) } +pub fn default_locked() -> Option { + Some(true) +} /// Available mod loaders to be used for a cluster. #[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default)] #[serde(rename_all = "lowercase")] pub enum Loader { - /// The default vanilla loader, no modding supported. - #[default] - Vanilla, - /// The MinecraftForge Minecraft mod loader. - Forge, - /// The FabircMC Minecraft mod loader. - Fabric, - /// The NeoForge Minecraft mod loader. - NeoForge, - /// The Quilt Minecraft mod loader. - Quilt, - /// The Legacy Fabric port mod loader. - LegacyFabric + /// The default vanilla loader, no modding supported. + #[default] + Vanilla, + /// The MinecraftForge Minecraft mod loader. + Forge, + /// The FabircMC Minecraft mod loader. + Fabric, + /// The NeoForge Minecraft mod loader. + NeoForge, + /// The Quilt Minecraft mod loader. + Quilt, + /// The Legacy Fabric port mod loader. + LegacyFabric, } impl std::fmt::Display for Loader { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match *self { - Self::Vanilla => "Vanilla", - Self::Forge => "Forge", - Self::Fabric => "Fabric", - Self::NeoForge => "NeoForge", - Self::Quilt => "Quilt", - Self::LegacyFabric => "LegacyFabric", - }) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match *self { + Self::Vanilla => "Vanilla", + Self::Forge => "Forge", + Self::Fabric => "Fabric", + Self::NeoForge => "NeoForge", + Self::Quilt => "Quilt", + Self::LegacyFabric => "LegacyFabric", + }) + } } impl Loader { - /// Get the loader version to lowercase for metadata fetching. - pub(crate) fn as_meta(&self) -> &'static str { - match *self { - Self::Vanilla => "vanilla", - Self::Forge => "forge", - Self::Fabric => "fabric", - Self::NeoForge => "neoforge", - Self::Quilt => "quilt", - Self::LegacyFabric => "legacyfabric", - } - } + /// Get the loader version to lowercase for metadata fetching. + pub(crate) fn as_meta(&self) -> &'static str { + match *self { + Self::Vanilla => "vanilla", + Self::Forge => "forge", + Self::Fabric => "fabric", + Self::NeoForge => "neoforge", + Self::Quilt => "quilt", + Self::LegacyFabric => "legacyfabric", + } + } } /// Custom Java arguments on a per-cluster basis. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct JavaOptions { - /// A custom java version from the global java store, if specified. - #[serde(skip_serializing_if = "Option::is_none")] - pub custom_version: Option, - /// Custom runtime arguments when running the cluster. - #[serde(skip_serializing_if = "Option::is_none")] - pub custom_arguments: Option>, - /// Custom environment variables when running the cluster. - #[serde(skip_serializing_if = "Option::is_none")] - pub custom_env_arguments: Option>, + /// A custom java version from the global java store, if specified. + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_version: Option, + /// Custom runtime arguments when running the cluster. + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_arguments: Option>, + /// Custom environment variables when running the cluster. + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_env_arguments: Option>, } impl Cluster { - /// Create a new Cluster - #[tracing::instrument] - pub async fn new(uuid: Uuid, name: String, version: String) -> crate::Result { - if name.trim().is_empty() { - return Err(anyhow::anyhow!("invalid instance name (empty)").into()); - } + /// Create a new Cluster + #[tracing::instrument] + pub async fn new(uuid: Uuid, name: String, version: String) -> crate::Result { + if name.trim().is_empty() { + return Err(anyhow::anyhow!("invalid instance name (empty)").into()); + } - Ok(Self { - uuid, - stage: ClusterStage::NotInstalled, - path: PathBuf::new().join(&name), - meta: ClusterMeta { - name, - group: vec![], - mc_version: version, - loader: Loader::Vanilla, - loader_version: None, - created_at: Utc::now(), - modified_at: Utc::now(), - package_data: None, - played_at: None, - overall_played: 0, - recently_played: 0, - icon: None, - icon_url: None, - }, - java: None, - memory: None, - resolution: None, - force_fullscreen: None, - init_hooks: None, - packages: HashMap::new(), - update: None, - }) - } + Ok(Self { + uuid, + stage: ClusterStage::NotInstalled, + path: PathBuf::new().join(&name), + meta: ClusterMeta { + name, + group: vec![], + mc_version: version, + loader: Loader::Vanilla, + loader_version: None, + created_at: Utc::now(), + modified_at: Utc::now(), + package_data: None, + played_at: None, + overall_played: 0, + recently_played: 0, + icon: None, + icon_url: None, + }, + java: None, + memory: None, + resolution: None, + force_fullscreen: None, + init_hooks: None, + packages: HashMap::new(), + update: None, + }) + } - /// Get the [`ClusterPath`] of the specified [`Cluster`]. - #[inline] - pub fn cluster_path(&self) -> ClusterPath { - ClusterPath::new(&self.path) - } + /// Get the [`ClusterPath`] of the specified [`Cluster`]. + #[inline] + pub fn cluster_path(&self) -> ClusterPath { + ClusterPath::new(&self.path) + } - /// Set the icon [`bytes::Bytes`] for this cluster. - #[tracing::instrument(skip(self, io_semaphore, icon))] - pub async fn set_icon<'a>( - &'a mut self, - cache_path: &Path, - io_semaphore: &IoSemaphore, - icon: bytes::Bytes, - file_name: &str, - ) -> crate::Result<()> { - let file = write_icon(file_name, cache_path, icon, io_semaphore).await?; - self.meta.icon = Some(file); - self.meta.modified_at = Utc::now(); - Ok(()) - } + /// Set the icon [`bytes::Bytes`] for this cluster. + #[tracing::instrument(skip(self, io_semaphore, icon))] + pub async fn set_icon<'a>( + &'a mut self, + cache_path: &Path, + io_semaphore: &IoSemaphore, + icon: bytes::Bytes, + file_name: &str, + ) -> crate::Result<()> { + let file = write_icon(file_name, cache_path, icon, io_semaphore).await?; + self.meta.icon = Some(file); + self.meta.modified_at = Utc::now(); + Ok(()) + } - /// Handle a cluster crash. - pub fn handle_crash(path: ClusterPath) { - tokio::task::spawn(async move { - let mut res = async { - let cluster = crate::api::cluster::get(&path, None).await?; - if let Some(cluster) = cluster { - if cluster.stage == ClusterStage::Installed { - send_message(&format!("cluster {} has crashed! visit the logs page for a crash report.", cluster.meta.name)).await?; - } - } + /// Handle a cluster crash. + pub fn handle_crash(path: ClusterPath) { + tokio::task::spawn(async move { + let mut res = async { + let cluster = crate::api::cluster::get(&path, None).await?; + if let Some(cluster) = cluster { + if cluster.stage == ClusterStage::Installed { + send_message(&format!( + "cluster {} has crashed! visit the logs page for a crash report.", + cluster.meta.name + )) + .await?; + } + } - Ok::<(), crate::Error>(()) - }.await; + Ok::<(), crate::Error>(()) + } + .await; - match res { - Ok(()) => {} - Err(err) => tracing::warn!("failed to send crash report to frontend {err}") - }; - }); - } + match res { + Ok(()) => {} + Err(err) => tracing::warn!("failed to send crash report to frontend {err}"), + }; + }); + } - /// sync all packages - pub fn sync_packages(cluster_path: ClusterPath, force: bool) { - let span = tracing::span!(tracing::Level::INFO, "sync_packages", ?cluster_path, ?force); - tokio::task::spawn(async move { - let result = async { - let _span = span.enter(); - let state = State::get().await?; - let cluster = crate::api::cluster::get(&cluster_path, None).await?; + /// sync all packages + pub fn sync_packages(cluster_path: ClusterPath, force: bool) { + let span = tracing::span!(tracing::Level::INFO, "sync_packages", ?cluster_path, ?force); + tokio::task::spawn(async move { + let result = async { + let _span = span.enter(); + let state = State::get().await?; + let cluster = crate::api::cluster::get(&cluster_path, None).await?; - if let Some(cluster) = cluster { - if cluster.stage != ClusterStage::PackDownloading || force { - let paths = cluster.get_full_subs().await?; - let caches_dir = state.directories.caches_dir().await; - let packages = crate::store::generate_context( - cluster.clone(), - paths, - caches_dir, - &state.io_semaphore, - &state.fetch_semaphore, - ).await?; + if let Some(cluster) = cluster { + if cluster.stage != ClusterStage::PackDownloading || force { + let paths = cluster.get_full_subs().await?; + let caches_dir = state.directories.caches_dir().await; + let packages = crate::store::generate_context( + cluster.clone(), + paths, + caches_dir, + &state.io_semaphore, + &state.fetch_semaphore, + ) + .await?; - let mut new_clusters = state.clusters.write().await; - if let Some(cluster) = new_clusters.0.get_mut(&cluster_path) { - cluster.packages = packages; - } + let mut new_clusters = state.clusters.write().await; + if let Some(cluster) = new_clusters.0.get_mut(&cluster_path) { + cluster.packages = packages; + } - send_cluster(cluster.uuid, &cluster_path, &cluster.meta.name, ClusterPayloadType::Synced).await?; - } - } else { - tracing::warn!("failed to fetch cluster packages: path {cluster_path} invalid."); - } - Ok::<(), crate::Error>(()) - }.await; + send_cluster( + cluster.uuid, + &cluster_path, + &cluster.meta.name, + ClusterPayloadType::Synced, + ) + .await?; + } + } else { + tracing::warn!( + "failed to fetch cluster packages: path {cluster_path} invalid." + ); + } + Ok::<(), crate::Error>(()) + } + .await; - match result { - Ok(()) => {} - Err(err) => tracing::warn!("failed to fetch cluster packages: {err}") - }; - }); - } + match result { + Ok(()) => {} + Err(err) => tracing::warn!("failed to fetch cluster packages: {err}"), + }; + }); + } - /// get the full path to the current cluster. - pub async fn get_full_path(&self) -> crate::Result { - let state = State::get().await?; - let clusters_dir = state.directories.clusters_dir().await; - Ok(clusters_dir.join(&self.path)) - } + /// get the full path to the current cluster. + pub async fn get_full_path(&self) -> crate::Result { + let state = State::get().await?; + let clusters_dir = state.directories.clusters_dir().await; + Ok(clusters_dir.join(&self.path)) + } - /// get full paths and subpaths - pub async fn get_full_subs(&self) -> crate::Result> { - let mut files = Vec::new(); - let cluster_path = self.get_full_path().await?; - let mut paths = |path: &str| { - let new = cluster_path.join(path); - if new.exists() { - for sub in std::fs::read_dir(&new).map_err(|e| IOError::with_path(e, &new))? { - let sub = sub.map_err(IOError::from)?.path(); - if sub.is_file() { files.push(sub); } - } - } - Ok::<(), crate::Error>(()) - }; + /// get full paths and subpaths + pub async fn get_full_subs(&self) -> crate::Result> { + let mut files = Vec::new(); + let cluster_path = self.get_full_path().await?; + let mut paths = |path: &str| { + let new = cluster_path.join(path); + if new.exists() { + for sub in std::fs::read_dir(&new).map_err(|e| IOError::with_path(e, &new))? { + let sub = sub.map_err(IOError::from)?.path(); + if sub.is_file() { + files.push(sub); + } + } + } + Ok::<(), crate::Error>(()) + }; - paths(PackageType::Mod.get_folder())?; - paths(PackageType::Shader.get_folder())?; - paths(PackageType::Resource.get_folder())?; - paths(PackageType::DataPack.get_folder())?; + paths(PackageType::Mod.get_folder())?; + paths(PackageType::Shader.get_folder())?; + paths(PackageType::Resource.get_folder())?; + paths(PackageType::DataPack.get_folder())?; - Ok(files) - } + Ok(files) + } - /// watch the filesystem for changes with [`notify`]. - #[tracing::instrument(skip(watcher))] - #[onelauncher_debug::debugger] - pub async fn watch(cluster_path: &Path, watcher: &mut Debouncer) -> crate::Result<()> { - async fn watch_path(cluster_path: &Path, watcher: &mut Debouncer, path: &str) -> crate::Result<()> { - let path = cluster_path.join(path); - io::create_dir_all(&path).await?; - watcher.watcher().watch(&cluster_path.join(path), notify::RecursiveMode::Recursive)?; - Ok(()) - } + /// watch the filesystem for changes with [`notify`]. + #[tracing::instrument(skip(watcher))] + #[onelauncher_debug::debugger] + pub async fn watch( + cluster_path: &Path, + watcher: &mut Debouncer, + ) -> crate::Result<()> { + async fn watch_path( + cluster_path: &Path, + watcher: &mut Debouncer, + path: &str, + ) -> crate::Result<()> { + let path = cluster_path.join(path); + io::create_dir_all(&path).await?; + watcher + .watcher() + .watch(&cluster_path.join(path), notify::RecursiveMode::Recursive)?; + Ok(()) + } - watch_path(cluster_path, watcher, PackageType::Mod.get_folder()).await?; - watch_path(cluster_path, watcher, PackageType::Shader.get_folder()).await?; - watch_path(cluster_path, watcher, PackageType::Resource.get_folder()).await?; - watch_path(cluster_path, watcher, PackageType::DataPack.get_folder()).await?; - watch_path(cluster_path, watcher, "crash-reports").await?; + watch_path(cluster_path, watcher, PackageType::Mod.get_folder()).await?; + watch_path(cluster_path, watcher, PackageType::Shader.get_folder()).await?; + watch_path(cluster_path, watcher, PackageType::Resource.get_folder()).await?; + watch_path(cluster_path, watcher, PackageType::DataPack.get_folder()).await?; + watch_path(cluster_path, watcher, "crash-reports").await?; - Ok(()) - } + Ok(()) + } } impl Clusters { - /// Initialize the cluster manager and HashMap. - #[tracing::instrument(skip(watcher))] - #[onelauncher_debug::debugger] - pub async fn initialize( - dirs: &Directories, - watcher: &mut Debouncer, - ) -> crate::Result { - let mut clusters = HashMap::new(); - let clusters_dir = dirs.clusters_dir().await; - io::create_dir_all(&&clusters_dir).await?; - watcher.watcher().watch(&clusters_dir, notify::RecursiveMode::NonRecursive)?; - let mut files = io::read_dir(&dirs.clusters_dir().await).await?; + /// Initialize the cluster manager and HashMap. + #[tracing::instrument(skip(watcher))] + #[onelauncher_debug::debugger] + pub async fn initialize( + dirs: &Directories, + watcher: &mut Debouncer, + ) -> crate::Result { + let mut clusters = HashMap::new(); + let clusters_dir = dirs.clusters_dir().await; + io::create_dir_all(&&clusters_dir).await?; + watcher + .watcher() + .watch(&clusters_dir, notify::RecursiveMode::NonRecursive)?; + let mut files = io::read_dir(&dirs.clusters_dir().await).await?; - while let Some(file) = files.next_entry().await.map_err(IOError::from)? { - let path = file.path(); - if path.is_dir() { - let cluster = match Self::from_dir(&path, dirs).await { - Ok(cluster) => Some(cluster), - Err(err) => { - tracing::warn!("failed to load cluster {err}. skipping"); - None - } - }; + while let Some(file) = files.next_entry().await.map_err(IOError::from)? { + let path = file.path(); + if path.is_dir() { + let cluster = match Self::from_dir(&path, dirs).await { + Ok(cluster) => Some(cluster), + Err(err) => { + tracing::warn!("failed to load cluster {err}. skipping"); + None + } + }; - if let Some(cluster_) = cluster { - let path = io::canonicalize(path)?; - Cluster::watch(&path, watcher).await?; - clusters.insert(cluster_.cluster_path(), cluster_); - } - } - } + if let Some(cluster_) = cluster { + let path = io::canonicalize(path)?; + Cluster::watch(&path, watcher).await?; + clusters.insert(cluster_.cluster_path(), cluster_); + } + } + } - Ok(Self(clusters)) - } + Ok(Self(clusters)) + } - /// update registered packages - #[tracing::instrument] - #[onelauncher_debug::debugger] - pub async fn update_packages() { - let res = async { - let state = State::get().await?; - let mut files: Vec<(Cluster, Vec)> = Vec::new(); + /// update registered packages + #[tracing::instrument] + #[onelauncher_debug::debugger] + pub async fn update_packages() { + let res = async { + let state = State::get().await?; + let mut files: Vec<(Cluster, Vec)> = Vec::new(); - { - let clusters = state.clusters.read().await; - for (_cluster_path, cluster) in clusters.0.iter() { - let paths = cluster.get_full_subs().await?; - files.push((cluster.clone(), paths)); - } - } + { + let clusters = state.clusters.read().await; + for (_cluster_path, cluster) in clusters.0.iter() { + let paths = cluster.get_full_subs().await?; + files.push((cluster.clone(), paths)); + } + } - let caches_dir = state.directories.caches_dir().await; - future::try_join_all(files.into_iter().map(|(cluster, files)| async { - let cluster_name = cluster.cluster_path(); - let packages = super::generate_context( - cluster, - files, - caches_dir.clone(), - &state.io_semaphore, - &state.fetch_semaphore, - ).await?; + let caches_dir = state.directories.caches_dir().await; + future::try_join_all(files.into_iter().map(|(cluster, files)| async { + let cluster_name = cluster.cluster_path(); + let packages = super::generate_context( + cluster, + files, + caches_dir.clone(), + &state.io_semaphore, + &state.fetch_semaphore, + ) + .await?; - let mut new_clusters = state.clusters.write().await; - if let Some(cluster) = new_clusters.0.get_mut(&cluster_name) { - cluster.packages = packages; - } + let mut new_clusters = state.clusters.write().await; + if let Some(cluster) = new_clusters.0.get_mut(&cluster_name) { + cluster.packages = packages; + } - drop(new_clusters); + drop(new_clusters); - Ok::<(), crate::Error>(()) - })).await?; + Ok::<(), crate::Error>(()) + })) + .await?; - { - let clusters = state.clusters.read().await; - clusters.sync().await?; - } + { + let clusters = state.clusters.read().await; + clusters.sync().await?; + } - Ok::<(), crate::Error>(()) - }.await; + Ok::<(), crate::Error>(()) + } + .await; - match res { - Ok(()) => {} - Err(err) => tracing::warn!("failed to fetch cluster packages: {err}") - }; - } + match res { + Ok(()) => {} + Err(err) => tracing::warn!("failed to fetch cluster packages: {err}"), + }; + } - /// update all available package versions - #[tracing::instrument] - #[onelauncher_debug::debugger] - pub async fn update_versions() { - let res = async { - let state = State::get().await?; - let mut updateable: Vec<(ClusterPath, String)> = Vec::new(); + /// update all available package versions + #[tracing::instrument] + #[onelauncher_debug::debugger] + pub async fn update_versions() { + let res = async { + let state = State::get().await?; + let mut updateable: Vec<(ClusterPath, String)> = Vec::new(); - { - let clusters = state.clusters.read().await; - for (cluster_path, cluster) in clusters.0.iter() { - if let Some(package_data) = &cluster.meta.package_data { - if let Some(linked_package) = &package_data.package_id { - updateable.push((cluster_path.clone(), linked_package.clone())); - } - } - } - } + { + let clusters = state.clusters.read().await; + for (cluster_path, cluster) in clusters.0.iter() { + if let Some(package_data) = &cluster.meta.package_data { + if let Some(linked_package) = &package_data.package_id { + updateable.push((cluster_path.clone(), linked_package.clone())); + } + } + } + } - // TODO + // TODO - { - let clusters = state.clusters.read().await; - clusters.sync().await?; - } + { + let clusters = state.clusters.read().await; + clusters.sync().await?; + } - Ok::<(), crate::Error>(()) - }.await; + Ok::<(), crate::Error>(()) + } + .await; - match res { - Ok(()) => {} - Err(err) => tracing::warn!("failed to update managed packages: {err}") - }; - } + match res { + Ok(()) => {} + Err(err) => tracing::warn!("failed to update managed packages: {err}"), + }; + } - /// insert a cluster into the HashMap - #[tracing::instrument(skip(self, cluster))] - #[onelauncher_debug::debugger] - pub async fn insert(&mut self, cluster: Cluster, dont_watch: bool) -> crate::Result<&Self> { - send_cluster( - cluster.uuid, - &cluster.cluster_path(), - &cluster.meta.name, - crate::proxy::ClusterPayloadType::Inserted - ).await?; + /// insert a cluster into the HashMap + #[tracing::instrument(skip(self, cluster))] + #[onelauncher_debug::debugger] + pub async fn insert(&mut self, cluster: Cluster, dont_watch: bool) -> crate::Result<&Self> { + send_cluster( + cluster.uuid, + &cluster.cluster_path(), + &cluster.meta.name, + crate::proxy::ClusterPayloadType::Inserted, + ) + .await?; - if !dont_watch { - let state = State::get().await?; - let mut watcher = state.watcher.write().await; - Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?; - } + if !dont_watch { + let state = State::get().await?; + let mut watcher = state.watcher.write().await; + Cluster::watch(&cluster.get_full_path().await?, &mut watcher).await?; + } - let cluster_name = cluster.cluster_path(); - cluster_name.validate()?; - self.0.insert(cluster_name, cluster); - Ok(self) - } + let cluster_name = cluster.cluster_path(); + cluster_name.validate()?; + self.0.insert(cluster_name, cluster); + Ok(self) + } - /// remove a cluster from the HashMap - #[tracing::instrument(skip(self))] - pub async fn remove(&mut self, cluster_path: &ClusterPath) -> crate::Result> { - let cluster = self.0.remove(cluster_path); - let path = cluster_path.full_path().await?; - if path.exists() { io::remove_dir_all(&path).await?; } + /// remove a cluster from the HashMap + #[tracing::instrument(skip(self))] + pub async fn remove(&mut self, cluster_path: &ClusterPath) -> crate::Result> { + let cluster = self.0.remove(cluster_path); + let path = cluster_path.full_path().await?; + if path.exists() { + io::remove_dir_all(&path).await?; + } - Ok(cluster) - } + Ok(cluster) + } - /// sync all available clusters - #[tracing::instrument(skip_all)] - pub async fn sync(&self) -> crate::Result<&Self> { - let _state = State::get().await?; - stream::iter(self.0.iter()) - .map(Ok::<_, crate::Error>) - .try_for_each_concurrent(None, |(_, cluster)| async move { - let json = serde_json::to_vec(&cluster)?; - let json_path = cluster.get_full_path().await?.join(CLUSTER_JSON); - io::write(&json_path, &json).await?; - Ok::<_, crate::Error>(()) - }).await?; + /// sync all available clusters + #[tracing::instrument(skip_all)] + pub async fn sync(&self) -> crate::Result<&Self> { + let _state = State::get().await?; + stream::iter(self.0.iter()) + .map(Ok::<_, crate::Error>) + .try_for_each_concurrent(None, |(_, cluster)| async move { + let json = serde_json::to_vec(&cluster)?; + let json_path = cluster.get_full_path().await?.join(CLUSTER_JSON); + io::write(&json_path, &json).await?; + Ok::<_, crate::Error>(()) + }) + .await?; - Ok(self) - } + Ok(self) + } - /// read a cluster from a directory - async fn from_dir(path: &Path, dirs: &Directories) -> crate::Result { - let json = io::read(&path.join(CLUSTER_JSON)).await?; - let mut cluster = serde_json::from_slice::(&json)?; - cluster.path = PathBuf::from(path.strip_prefix(dirs.clusters_dir().await).map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?); - Ok(cluster) - } + /// read a cluster from a directory + async fn from_dir(path: &Path, dirs: &Directories) -> crate::Result { + let json = io::read(&path.join(CLUSTER_JSON)).await?; + let mut cluster = serde_json::from_slice::(&json)?; + cluster.path = PathBuf::from( + path.strip_prefix(dirs.clusters_dir().await) + .map_err(|err| anyhow::anyhow!("failed to strip prefix {err}"))?, + ); + Ok(cluster) + } - /// sync all available clusters - pub fn sync_clusters(cluster_path: ClusterPath) { - tokio::task::spawn(async move { - let span = tracing::span!(tracing::Level::INFO, "sync_clusters"); - let res = async { - let _span = span.enter(); - let state = State::get().await?; - let dirs = &state.directories; - let mut clusters = state.clusters.write().await; + /// sync all available clusters + pub fn sync_clusters(cluster_path: ClusterPath) { + tokio::task::spawn(async move { + let span = tracing::span!(tracing::Level::INFO, "sync_clusters"); + let res = async { + let _span = span.enter(); + let state = State::get().await?; + let dirs = &state.directories; + let mut clusters = state.clusters.write().await; - if let Some(cluster) = clusters.0.get_mut(&cluster_path) { - if !cluster.get_full_path().await?.exists() { - send_cluster( - cluster.uuid, - &cluster_path, - &cluster.meta.name, - crate::proxy::ClusterPayloadType::Deleted - ).await?; - tracing::debug!("removed non-existant fs cluster!"); - clusters.0.remove(&cluster_path); - } - } else if cluster_path.full_path().await?.exists() { - clusters.insert(Self::from_dir(&cluster_path.full_path().await?, dirs).await?, false).await?; - Cluster::sync_packages(cluster_path, false); - } - Ok::<(), crate::Error>(()) - }.await; + if let Some(cluster) = clusters.0.get_mut(&cluster_path) { + if !cluster.get_full_path().await?.exists() { + send_cluster( + cluster.uuid, + &cluster_path, + &cluster.meta.name, + crate::proxy::ClusterPayloadType::Deleted, + ) + .await?; + tracing::debug!("removed non-existant fs cluster!"); + clusters.0.remove(&cluster_path); + } + } else if cluster_path.full_path().await?.exists() { + clusters + .insert( + Self::from_dir(&cluster_path.full_path().await?, dirs).await?, + false, + ) + .await?; + Cluster::sync_packages(cluster_path, false); + } + Ok::<(), crate::Error>(()) + } + .await; - match res { - Ok(()) => {} - Err(err) => tracing::warn!("failed to fetch a cluster: {err}") - }; - }); - } + match res { + Ok(()) => {} + Err(err) => tracing::warn!("failed to fetch a cluster: {err}"), + }; + }); + } } diff --git a/core/src/store/credentials.rs b/core/src/store/credentials.rs index d892f87a..312b78b5 100644 --- a/core/src/store/credentials.rs +++ b/core/src/store/credentials.rs @@ -1,69 +1,81 @@ //! Core credential management and storage with IOTA Stronghold. -use crate::utils::http::IoSemaphore; -use serde::{Serialize, Serializer}; -use iota_stronghold::{KeyProvider, SnapshotPath}; -use zeroize::Zeroizing; -use std::{ops::Deref, path::Path}; use super::Directories; +use crate::utils::http::IoSemaphore; +use iota_stronghold::{KeyProvider, SnapshotPath}; +use serde::{Serialize, Serializer}; +use std::ops::Deref; +use std::path::Path; +use zeroize::Zeroizing; /// A K/V encrypted store to handle digital secrets using IOTA Stronghold. #[derive(Debug)] pub struct Credentials { - inner: iota_stronghold::Stronghold, - path: iota_stronghold::SnapshotPath, - keyprovider: iota_stronghold::KeyProvider, + inner: iota_stronghold::Stronghold, + path: iota_stronghold::SnapshotPath, + keyprovider: iota_stronghold::KeyProvider, } impl Credentials { - /// Initialize the K/V encrypted stronghold. - pub fn new>( - path: P, - password: Zeroizing>, - dirs: &Directories, - io_semaphore: &IoSemaphore, - ) -> crate::Result { - let path = SnapshotPath::from_path(path); - let stronghold = iota_stronghold::Stronghold::default(); - let keyprovider = KeyProvider::try_from(password).map_err(|e| StrongholdError::MemoryError(e))?; - if path.exists() { stronghold.load_snapshot(&keyprovider, &path).map_err(|e| StrongholdError::ClientError(e))?; } - Ok(Self { inner: stronghold, path, keyprovider }) - } + /// Initialize the K/V encrypted stronghold. + pub fn new>( + path: P, + password: Zeroizing>, + dirs: &Directories, + io_semaphore: &IoSemaphore, + ) -> crate::Result { + let path = SnapshotPath::from_path(path); + let stronghold = iota_stronghold::Stronghold::default(); + let keyprovider = + KeyProvider::try_from(password).map_err(|e| StrongholdError::MemoryError(e))?; + if path.exists() { + stronghold + .load_snapshot(&keyprovider, &path) + .map_err(|e| StrongholdError::ClientError(e))?; + } + Ok(Self { + inner: stronghold, + path, + keyprovider, + }) + } - pub fn save(&self) -> crate::Result<()> { - self.inner.commit_with_keyprovider(&self.path, &self.keyprovider).map_err(|e| StrongholdError::ClientError(e))?; - Ok(()) - } + pub fn save(&self) -> crate::Result<()> { + self.inner + .commit_with_keyprovider(&self.path, &self.keyprovider) + .map_err(|e| StrongholdError::ClientError(e))?; + Ok(()) + } - pub fn inner(&self) -> &iota_stronghold::Stronghold { - &self.inner - } + pub fn inner(&self) -> &iota_stronghold::Stronghold { + &self.inner + } } impl Deref for Credentials { - type Target = iota_stronghold::Stronghold; - fn deref(&self) -> &Self::Target { - &self.inner - } + type Target = iota_stronghold::Stronghold; + fn deref(&self) -> &Self::Target { + &self.inner + } } #[derive(thiserror::Error, Debug)] pub enum StrongholdError { - #[error("stronghold not initialized")] - StrongholdNotInitialized, - #[error(transparent)] - ClientError(#[from] iota_stronghold::ClientError), - #[error(transparent)] - MemoryError(#[from] iota_stronghold::MemoryError), - #[error(transparent)] - ProcedureError(#[from] iota_stronghold::procedures::ProcedureError), + #[error("stronghold not initialized")] + StrongholdNotInitialized, + #[error(transparent)] + ClientError(#[from] iota_stronghold::ClientError), + #[error(transparent)] + MemoryError(#[from] iota_stronghold::MemoryError), + #[error(transparent)] + ProcedureError(#[from] iota_stronghold::procedures::ProcedureError), } impl Serialize for StrongholdError { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_str()) - } + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } } diff --git a/core/src/store/dirs.rs b/core/src/store/dirs.rs index 19917481..0a7b30a7 100644 --- a/core/src/store/dirs.rs +++ b/core/src/store/dirs.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use tokio::sync::RwLock; -use crate::constants; use super::Settings; +use crate::constants; /// The public `settings.json` file used to store the [`Settings`] state. pub const SETTINGS_FILE: &str = "settings.json"; @@ -17,170 +17,173 @@ pub const METADATA_FOLDER: &str = "metadata"; /// 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, - /// The current working directory of the directory manager. - pub cwd: PathBuf, + /// 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, + /// The current working directory of the directory manager. + pub cwd: PathBuf, } impl Directories { - /// Bootstrap the initial settings directory on first startup. - pub fn init_settings_dir() -> Option { - Self::env_path("ONELAUNCHER_CONFIG").or_else(|| Some(dirs::config_dir()?.join(constants::APP_CONFIG_DIR))) - } + /// Bootstrap the initial settings directory on first startup. + pub fn init_settings_dir() -> Option { + Self::env_path("ONELAUNCHER_CONFIG") + .or_else(|| Some(dirs::config_dir()?.join(constants::APP_CONFIG_DIR))) + } - /// Bootstrap the initial settings directory on first startup. - #[inline] - pub fn init_settings_file() -> crate::Result { - let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?; - Ok(settings_dir.join("settings.json")) - } + /// Bootstrap the initial settings directory on first startup. + #[inline] + pub fn init_settings_file() -> crate::Result { + 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 { - let cwd = std::env::current_dir().map_err(|err| DirectoryError::WorkingDir(err))?; - 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, - }) - } + /// Initialize the core directory manager + #[tracing::instrument] + pub fn initalize(settings: &Settings) -> crate::Result { + let cwd = std::env::current_dir().map_err(|err| DirectoryError::WorkingDir(err))?; + 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(METADATA_FOLDER) - } + /// 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(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/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/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") - } + /// 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] - pub fn logs_dir() -> Option { - Self::init_settings_dir() - .map(|d| d.join("logs")) - } + /// Bootstrap the core logs directory and get it as a [`PathBuf`]. + #[inline] + pub fn logs_dir() -> Option { + 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(SETTINGS_FILE) - } + /// Get the `settings_dir/settings.json` file as a [`PathBuf`]. + #[inline] + pub fn settings_file(&self) -> PathBuf { + self.settings_dir.join(SETTINGS_FILE) + } - /// Get a [`PathBuf`] from a provided environment variable. - #[inline] - fn env_path(name: &str) -> Option { - std::env::var_os(name).map(PathBuf::from) - } + /// Get a [`PathBuf`] from a provided environment variable. + #[inline] + fn env_path(name: &str) -> Option { + 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` 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/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` 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/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` 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 `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 { - Ok(cluster_path.full_path().await?.join("logs")) - } + /// Get the `{cluster_path}/logs` directory. + #[inline] + pub async fn cluster_logs_dir(cluster_path: &super::ClusterPath) -> crate::Result { + Ok(cluster_path.full_path().await?.join("logs")) + } - /// 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/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` 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 `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` 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 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") - } + /// 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. + /// 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), + /// A wrapper over [`std::io::Error`] for cwd fetching errors. + #[error("could not open working directory {0}")] + WorkingDir(std::io::Error), } diff --git a/core/src/store/discord.rs b/core/src/store/discord.rs index c2568ec7..a580de62 100644 --- a/core/src/store/discord.rs +++ b/core/src/store/discord.rs @@ -1,5 +1,6 @@ //! Handles Discord rich prescence integration -use std::sync::{atomic::AtomicBool, Arc}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; use discord_rich_presence::activity::{Activity, Assets}; use discord_rich_presence::{DiscordIpc, DiscordIpcClient}; @@ -10,141 +11,151 @@ use crate::State; /// A structure used to manage a Discord IPC client. pub struct DiscordRPC { - /// The managed [`DiscordIpcClient`] used for this IPC client. - client: Arc>, - /// Whether or not we are connected to the IPC server and internet. - connected: Arc, + /// The managed [`DiscordIpcClient`] used for this IPC client. + client: Arc>, + /// Whether or not we are connected to the IPC server and internet. + connected: Arc, } impl DiscordRPC { - /// Initialize the Discord IPC client and attempt to connect to the server. - /// If an error is returned, it will be ignored since Discord RPC isnt essential for the launcher to load. - pub async fn initialize(is_offline: bool) -> crate::Result { - let mut discord_ipc = - DiscordIpcClient::new(DISCORD_RPC_CLIENT_ID).map_err(|e| { - anyhow::anyhow!("failed to create discord client {}", e) - })?; + /// Initialize the Discord IPC client and attempt to connect to the server. + /// If an error is returned, it will be ignored since Discord RPC isnt essential for the launcher to load. + pub async fn initialize(is_offline: bool) -> crate::Result { + let mut discord_ipc = DiscordIpcClient::new(DISCORD_RPC_CLIENT_ID) + .map_err(|e| anyhow::anyhow!("failed to create discord client {}", e))?; - let connected = if !is_offline { - let result = discord_ipc.connect(); - if result.is_ok() { - Arc::new(AtomicBool::new(true)) - } else { - Arc::new(AtomicBool::new(false)) - } - } else { - Arc::new(AtomicBool::new(false)) - }; + let connected = if !is_offline { + let result = discord_ipc.connect(); + if result.is_ok() { + Arc::new(AtomicBool::new(true)) + } else { + Arc::new(AtomicBool::new(false)) + } + } else { + Arc::new(AtomicBool::new(false)) + }; - let client = Arc::new(RwLock::new(discord_ipc)); - Ok(DiscordRPC { client, connected }) - } + let client = Arc::new(RwLock::new(discord_ipc)); + Ok(DiscordRPC { client, connected }) + } - /// Set the Discord IPC activity with a message and try to reconnect if the initial connection fails. - pub async fn set_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> { - if !self.retry_online().await { return Ok(()); } - let state = State::get().await?; - let settings = state.settings.read().await; - if settings.disable_discord { - Ok(self.clear_activity(true).await?) - } else { - Ok(self.apply_activity(msg, reconnect_if_fail).await?) - } - } + /// Set the Discord IPC activity with a message and try to reconnect if the initial connection fails. + pub async fn set_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> { + if !self.retry_online().await { + return Ok(()); + } + let state = State::get().await?; + let settings = state.settings.read().await; + if settings.disable_discord { + Ok(self.clear_activity(true).await?) + } else { + Ok(self.apply_activity(msg, reconnect_if_fail).await?) + } + } - /// Do not use this function, use [`DiscordRPC#set_activity`] - pub async fn apply_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> { - if !self.retry_connection().await { return Ok(()); } + /// Do not use this function, use [`DiscordRPC#set_activity`] + pub async fn apply_activity(&self, msg: &str, reconnect_if_fail: bool) -> crate::Result<()> { + if !self.retry_connection().await { + return Ok(()); + } - let activity = Activity::new().state(msg).assets( - Assets::new().large_image("polyfrost").large_text("Polyfrost Logo") - ); + let activity = Activity::new().state(msg).assets( + Assets::new() + .large_image("polyfrost") + .large_text("Polyfrost Logo"), + ); - let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> = self.client.write().await; - let result = client.set_activity(activity.clone()); - let could_not_set = |e: Box| { - anyhow::anyhow!("failed to apply discord activity {}", e) - }; + let mut client: tokio::sync::RwLockWriteGuard<'_, DiscordIpcClient> = + self.client.write().await; + let result = client.set_activity(activity.clone()); + let could_not_set = |e: Box| { + anyhow::anyhow!("failed to apply discord activity {}", e) + }; - if reconnect_if_fail { - if let Err(_e) = result { - client.reconnect().map_err(|e| { - anyhow::anyhow!("failed to reconnect to Discord RPC {}", e) - })?; + if reconnect_if_fail { + if let Err(_e) = result { + client + .reconnect() + .map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?; - return Ok(client.set_activity(activity).map_err(could_not_set)?); - } - } else { - result.map_err(could_not_set)?; - } + return Ok(client.set_activity(activity).map_err(could_not_set)?); + } + } else { + result.map_err(could_not_set)?; + } - Ok(()) - } + Ok(()) + } - /// Attempt to check if we are still in an offline environment. - pub async fn retry_online(&self) -> bool { - let state = match State::get().await { - Ok(s) => s, - Err(_) => return false, - }; + /// Attempt to check if we are still in an offline environment. + pub async fn retry_online(&self) -> bool { + let state = match State::get().await { + Ok(s) => s, + Err(_) => return false, + }; - let offline = state.offline.read().await; - if *offline { return false; } - true - } + let offline = state.offline.read().await; + if *offline { + return false; + } + true + } - /// Retry the Discord IPC connection. - pub async fn retry_connection(&self) -> bool { - let mut client = self.client.write().await; - if !self.connected.load(std::sync::atomic::Ordering::Relaxed) { - if client.connect().is_ok() { - self.connected.store(true, std::sync::atomic::Ordering::Relaxed); - return true; - } - return false; - } - true - } + /// Retry the Discord IPC connection. + pub async fn retry_connection(&self) -> bool { + let mut client = self.client.write().await; + if !self.connected.load(std::sync::atomic::Ordering::Relaxed) { + if client.connect().is_ok() { + self.connected + .store(true, std::sync::atomic::Ordering::Relaxed); + return true; + } + return false; + } + true + } - /// Clear the current Discord IPC activity, reconnecting if requested. - pub async fn clear_activity(&self, reconnect_if_fail: bool) -> crate::Result<()> { - if !self.retry_online().await || !self.retry_connection().await { return Ok(()); } - - let mut client = self.client.write().await; - let result = client.clear_activity(); - let could_not_clear = |e: Box| { - anyhow::anyhow!("failed to clear discord activity {}", e) - }; + /// Clear the current Discord IPC activity, reconnecting if requested. + pub async fn clear_activity(&self, reconnect_if_fail: bool) -> crate::Result<()> { + if !self.retry_online().await || !self.retry_connection().await { + return Ok(()); + } - if reconnect_if_fail { - if result.is_err() { - client.reconnect().map_err(|e| { - anyhow::anyhow!("failed to reconnect to Discord RPC {}", e) - })?; + let mut client = self.client.write().await; + let result = client.clear_activity(); + let could_not_clear = |e: Box| { + anyhow::anyhow!("failed to clear discord activity {}", e) + }; - return Ok(client.clear_activity().map_err(could_not_clear)?); - } - } else { - result.map_err(could_not_clear)?; - } + if reconnect_if_fail { + if result.is_err() { + client + .reconnect() + .map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?; - Ok(()) - } + return Ok(client.clear_activity().map_err(could_not_clear)?); + } + } else { + result.map_err(could_not_clear)?; + } - /// Clear and disable the Discord IPC, reconnecting if requested. - pub async fn clear(&self, reconnect_if_fail: bool) -> crate::Result<()> { - let state: Arc> = State::get().await?; + Ok(()) + } - { - let settings = state.settings.read().await; - if settings.disable_discord { - println!("discord is disabled.. clearing"); - return self.clear_activity(true).await; - } - } + /// Clear and disable the Discord IPC, reconnecting if requested. + pub async fn clear(&self, reconnect_if_fail: bool) -> crate::Result<()> { + let state: Arc> = State::get().await?; - self.set_activity("Idling...", reconnect_if_fail).await?; + { + let settings = state.settings.read().await; + if settings.disable_discord { + println!("discord is disabled.. clearing"); + return self.clear_activity(true).await; + } + } - Ok(()) - } + self.set_activity("Idling...", reconnect_if_fail).await?; + + Ok(()) + } } diff --git a/core/src/store/flags.rs b/core/src/store/flags.rs index 13166cc9..c3b2029b 100644 --- a/core/src/store/flags.rs +++ b/core/src/store/flags.rs @@ -1,2 +1 @@ //! Global Package Metadata management. - diff --git a/core/src/store/java_versions.rs b/core/src/store/java_versions.rs index 57b92f93..f4865350 100644 --- a/core/src/store/java_versions.rs +++ b/core/src/store/java_versions.rs @@ -1,73 +1,71 @@ //! Handles all available Java installations +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use serde::{Serialize, Deserialize}; use std::path::PathBuf; -use crate::utils::java::JavaVersion; use crate::utils::java; +use crate::utils::java::JavaVersion; /// A HashMap of all located and installed available Java versions #[derive(Debug, Serialize, Deserialize, Clone)] pub struct JavaVersions(HashMap); impl Default for JavaVersions { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl JavaVersions { - /// Create an empty JavaVersions `HashMap`. - pub fn new() -> JavaVersions { - JavaVersions(HashMap::new()) - } + /// Create an empty JavaVersions `HashMap`. + pub fn new() -> JavaVersions { + JavaVersions(HashMap::new()) + } - /// Inserts a key-value pair into the map. - pub fn insert(&mut self, key: String, java: JavaVersion) { - self.0.insert(key, java); - } + /// Inserts a key-value pair into the map. + pub fn insert(&mut self, key: String, java: JavaVersion) { + self.0.insert(key, java); + } - /// Removes a key from the map, returning the value at the key if the key was previously in the map. - pub fn remove(&mut self, key: &String) { - self.0.remove(key); - } + /// Removes a key from the map, returning the value at the key if the key was previously in the map. + pub fn remove(&mut self, key: &String) { + self.0.remove(key); + } - /// Returns a reference to the value corresponding to the key. - pub fn get(&self, key: &String) -> Option<&JavaVersion> { - self.0.get(key) - } + /// Returns a reference to the value corresponding to the key. + pub fn get(&self, key: &String) -> Option<&JavaVersion> { + self.0.get(key) + } - /// Returns a mutable reference to the value corresponding to the key. - pub fn get_mut(&mut self, key: &String) -> Option<&mut JavaVersion> { - self.0.get_mut(key) - } + /// Returns a mutable reference to the value corresponding to the key. + pub fn get_mut(&mut self, key: &String) -> Option<&mut JavaVersion> { + self.0.get_mut(key) + } - /// Returns the number of elements in the map. - pub fn count(&self) -> usize { - self.0.len() - } + /// Returns the number of elements in the map. + pub fn count(&self) -> usize { + self.0.len() + } - /// A collection visiting all keys in arbitrary order. - pub fn keys(&self) -> Vec { - self.0.keys().cloned().collect() - } + /// A collection visiting all keys in arbitrary order. + pub fn keys(&self) -> Vec { + self.0.keys().cloned().collect() + } - /// Validates all stored java versions. - pub async fn validate(&self) -> bool { - for (_, java) in self.0.iter() { - let runtime = java::check_java_instance( - PathBuf::from(&java.path).as_path() - ).await; - if let Some(runtime) = runtime { - if runtime.version != java.version { - return false; - } - } else { - return false; - } - } + /// Validates all stored java versions. + pub async fn validate(&self) -> bool { + for (_, java) in self.0.iter() { + let runtime = java::check_java_instance(PathBuf::from(&java.path).as_path()).await; + if let Some(runtime) = runtime { + if runtime.version != java.version { + return false; + } + } else { + return false; + } + } - true - } + true + } } diff --git a/core/src/store/metadata.rs b/core/src/store/metadata.rs index 489adcce..38e88c5f 100644 --- a/core/src/store/metadata.rs +++ b/core/src/store/metadata.rs @@ -1,8 +1,10 @@ //! Handles fetching metadata via interpulse. use interpulse::api::minecraft::{fetch_version_manifest, VersionManifest as MinecraftManifest}; -use interpulse::api::modded::{fetch_manifest as fetch_modded_manifest, Manifest as ModdedManifest}; -use serde::{Serialize, Deserialize}; +use interpulse::api::modded::{ + fetch_manifest as fetch_modded_manifest, Manifest as ModdedManifest, +}; +use serde::{Deserialize, Serialize}; use crate::utils::http::{read_json, write, IoSemaphore}; use crate::utils::io::copy; @@ -21,136 +23,155 @@ const METADATA_URL: &str = "localhost:5543"; /// A structure of manifests and metadata fetching utilities. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Metadata { - /// The [`MinecraftManifest`] associated with core Minecraft versions. - pub minecraft: MinecraftManifest, - /// The [`ModdedManifest`] associated with core Fabric versions. - pub fabric: ModdedManifest, - /// The [`ModdedManifest`] associated with core Quilt versions. - pub quilt: ModdedManifest, - /// The [`ModdedManifest`] associated with core NeoForge versions. - pub neoforge: ModdedManifest, - /// The [`ModdedManifest`] associated with core Forge versions. - pub forge: ModdedManifest, - /// The [`ModdedManifest`] associated with core Legacy Fabric versions. - pub legacy_fabric: ModdedManifest, + /// The [`MinecraftManifest`] associated with core Minecraft versions. + pub minecraft: MinecraftManifest, + /// The [`ModdedManifest`] associated with core Fabric versions. + pub fabric: ModdedManifest, + /// The [`ModdedManifest`] associated with core Quilt versions. + pub quilt: ModdedManifest, + /// The [`ModdedManifest`] associated with core NeoForge versions. + pub neoforge: ModdedManifest, + /// The [`ModdedManifest`] associated with core Forge versions. + pub forge: ModdedManifest, + /// The [`ModdedManifest`] associated with core Legacy Fabric versions. + pub legacy_fabric: ModdedManifest, } impl Metadata { - /// Get the formatted manifest for a specific [`Metadata`] type. - fn get_manifest(name: &str) -> String { - format!("{METADATA_URL}/{name}/v0/manifest.json") - } + /// Get the formatted manifest for a specific [`Metadata`] type. + fn get_manifest(name: &str) -> String { + format!("{METADATA_URL}/{name}/v0/manifest.json") + } - /// Fetch all available metadata types and get a new [`Metadata`] structure. - pub async fn fetch() -> crate::Result { - let (minecraft, fabric, quilt, neoforge, forge, legacy_fabric) = tokio::try_join! { - async { - let url = Self::get_manifest("minecraft"); - fetch_version_manifest(Some(&url)).await - }, - async { - let url = Self::get_manifest("fabric"); - fetch_modded_manifest(&url).await - }, - async { - let url = Self::get_manifest("quilt"); - fetch_modded_manifest(&url).await - }, - async { - let url = Self::get_manifest("neo"); - fetch_modded_manifest(&url).await - }, - async { - let url = Self::get_manifest("forge"); - fetch_modded_manifest(&url).await - }, - async { - let url = Self::get_manifest("legacy-fabric"); - fetch_modded_manifest(&url).await - }, - }?; + /// Fetch all available metadata types and get a new [`Metadata`] structure. + pub async fn fetch() -> crate::Result { + let (minecraft, fabric, quilt, neoforge, forge, legacy_fabric) = tokio::try_join! { + async { + let url = Self::get_manifest("minecraft"); + fetch_version_manifest(Some(&url)).await + }, + async { + let url = Self::get_manifest("fabric"); + fetch_modded_manifest(&url).await + }, + async { + let url = Self::get_manifest("quilt"); + fetch_modded_manifest(&url).await + }, + async { + let url = Self::get_manifest("neo"); + fetch_modded_manifest(&url).await + }, + async { + let url = Self::get_manifest("forge"); + fetch_modded_manifest(&url).await + }, + async { + let url = Self::get_manifest("legacy-fabric"); + fetch_modded_manifest(&url).await + }, + }?; - Ok(Self { - minecraft, - fabric, - quilt, - neoforge, - forge, - legacy_fabric, - }) - } + Ok(Self { + minecraft, + fabric, + quilt, + neoforge, + forge, + legacy_fabric, + }) + } - /// Initialize the core Metadata manager. - #[tracing::instrument(skip(io_semaphore))] - #[onelauncher_debug::debugger] - pub async fn initialize(dirs: &Directories, online: bool, io_semaphore: &IoSemaphore) -> crate::Result { - let mut metadata = None; - let path = dirs.caches_dir().await.join("metadata.json"); - let backup = dirs.caches_dir().await.join("metadata.json.bak"); + /// Initialize the core Metadata manager. + #[tracing::instrument(skip(io_semaphore))] + #[onelauncher_debug::debugger] + pub async fn initialize( + dirs: &Directories, + online: bool, + io_semaphore: &IoSemaphore, + ) -> crate::Result { + let mut metadata = None; + let path = dirs.caches_dir().await.join("metadata.json"); + let backup = dirs.caches_dir().await.join("metadata.json.bak"); - if let Ok(metadata_json) = read_json::(&path, io_semaphore).await { - metadata = Some(metadata_json); - } else if online { - let res = async { - let fetch_data = Self::fetch().await?; - - write(&path, &serde_json::to_vec(&fetch_data).unwrap_or_default(), io_semaphore).await?; - write(&backup, &serde_json::to_vec(&fetch_data).unwrap_or_default(), io_semaphore).await?; + if let Ok(metadata_json) = read_json::(&path, io_semaphore).await { + metadata = Some(metadata_json); + } else if online { + let res = async { + let fetch_data = Self::fetch().await?; - metadata = Some(fetch_data); - Ok::<(), crate::Error>(()) - }.await; + write( + &path, + &serde_json::to_vec(&fetch_data).unwrap_or_default(), + io_semaphore, + ) + .await?; + write( + &backup, + &serde_json::to_vec(&fetch_data).unwrap_or_default(), + io_semaphore, + ) + .await?; - match res { - Ok(()) => {} - Err(err) => { tracing::warn!("failed to fetch metadata: {err}") } - } - } else if let Ok(metadata_json) = read_json::(&backup, io_semaphore).await { - metadata = Some(metadata_json); - copy(&backup, &path).await?; - } + metadata = Some(fetch_data); + Ok::<(), crate::Error>(()) + } + .await; - if let Some(meta) = metadata { - Ok(meta) - } else { - Err(anyhow::anyhow!("failed to fetch launcher metadata").into()) - } - } + match res { + Ok(()) => {} + Err(err) => { + tracing::warn!("failed to fetch metadata: {err}") + } + } + } else if let Ok(metadata_json) = read_json::(&backup, io_semaphore).await { + metadata = Some(metadata_json); + copy(&backup, &path).await?; + } - /// Update and backup all available metadata. - pub async fn update() { - let res = async { - let fetch_data = Metadata::fetch().await?; - let state = State::get().await?; + if let Some(meta) = metadata { + Ok(meta) + } else { + Err(anyhow::anyhow!("failed to fetch launcher metadata").into()) + } + } - let path = state - .directories - .caches_dir() - .await - .join("metadata.json"); - let backup = state - .directories - .caches_dir() - .await - .join("metadata.json.bak"); + /// Update and backup all available metadata. + pub async fn update() { + let res = async { + let fetch_data = Metadata::fetch().await?; + let state = State::get().await?; - if path.exists() { - copy(&path, &backup).await?; - } + let path = state.directories.caches_dir().await.join("metadata.json"); + let backup = state + .directories + .caches_dir() + .await + .join("metadata.json.bak"); - write(&path, &serde_json::to_vec(&fetch_data)?, &state.io_semaphore).await?; + if path.exists() { + copy(&path, &backup).await?; + } - let mut old_metadata = state.metadata.write().await; - *old_metadata = fetch_data; + write( + &path, + &serde_json::to_vec(&fetch_data)?, + &state.io_semaphore, + ) + .await?; - Ok::<(), crate::Error>(()) - }.await; - - match res { - Ok(()) => {} - Err(err) => { - tracing::warn!("failed to update launcher metadata: {err}") - } - }; - } + let mut old_metadata = state.metadata.write().await; + *old_metadata = fetch_data; + + Ok::<(), crate::Error>(()) + } + .await; + + match res { + Ok(()) => {} + Err(err) => { + tracing::warn!("failed to update launcher metadata: {err}") + } + }; + } } diff --git a/core/src/store/minecraft.rs b/core/src/store/minecraft.rs index 3ab181f6..a4250c50 100644 --- a/core/src/store/minecraft.rs +++ b/core/src/store/minecraft.rs @@ -1,16 +1,16 @@ //! Handles MSA Authentication flow. -use std::collections::HashMap; use base64::Engine; -use rand::Rng; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use uuid::Uuid; use chrono::{DateTime, Utc}; use p256::ecdsa::signature::Signer; use p256::ecdsa::SigningKey; use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use serde_json::json; use sha2::Digest; +use std::collections::HashMap; +use uuid::Uuid; const AUTH_STORE: &str = "authentication.json"; @@ -28,9 +28,14 @@ pub struct MinecraftState { impl MinecraftState { /// Initialize a new Minecraft global state. #[tracing::instrument] - pub async fn initialize(dirs: &super::Directories, io_semaphore: &crate::utils::http::IoSemaphore) -> crate::Result { + pub async fn initialize( + dirs: &super::Directories, + io_semaphore: &crate::utils::http::IoSemaphore, + ) -> crate::Result { let path = dirs.caches_dir().await.join(AUTH_STORE); - let store = crate::utils::http::read_json(&path, io_semaphore).await.ok(); + let store = crate::utils::http::read_json(&path, io_semaphore) + .await + .ok(); if let Some(store) = store { Ok(store) @@ -113,15 +118,30 @@ impl MinecraftState { let hash_result = hash.finalize(); let challenge = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(&hash_result); let (sid, ruri) = sisu_authenticate(&token.token, &challenge, &key).await?; - Ok(MinecraftLogin { verify, challenge, session_id: sid, redirect_uri: ruri.msa_oauth_redirect }) + Ok(MinecraftLogin { + verify, + challenge, + session_id: sid, + redirect_uri: ruri.msa_oauth_redirect, + }) } /// Finish a Microsoft authentication flow. #[tracing::instrument(skip(self))] - pub async fn finish(&mut self, code: &str, flow: MinecraftLogin) -> crate::Result { + pub async fn finish( + &mut self, + code: &str, + flow: MinecraftLogin, + ) -> crate::Result { let (key, token) = self.refresh().await?; let oauth_token = oauth_token(code, &flow.verify).await?; - let sisu_authorize = sisu_authorize(Some(&flow.session_id), &oauth_token.access_token, &token.token, &key).await?; + let sisu_authorize = sisu_authorize( + Some(&flow.session_id), + &oauth_token.access_token, + &token.token, + &key, + ) + .await?; let xbox_token = xsts_authorize(sisu_authorize, &token.token, &key).await?; let minecraft_token = minecraft_token(xbox_token).await?; minecraft_entitlements(&minecraft_token.access_token).await?; @@ -132,7 +152,8 @@ impl MinecraftState { username: profile.name, access_token: minecraft_token.access_token, refresh_token: oauth_token.refresh_token, - #[allow(deprecated)] expires: Utc::now() + chrono::TimeDelta::seconds(oauth_token.expires_in as i64), + #[allow(deprecated)] + expires: Utc::now() + chrono::TimeDelta::seconds(oauth_token.expires_in as i64), }; self.users.insert(profile_id, credentials.clone()); @@ -171,12 +192,8 @@ impl MinecraftState { let oauth_token = oauth_refresh(&creds.refresh_token).await?; let (key, token) = self.refresh().await?; - let sisu_authorize = sisu_authorize( - None, - &oauth_token.access_token, - &token.token, - &key, - ).await?; + let sisu_authorize = + sisu_authorize(None, &oauth_token.access_token, &token.token, &key).await?; let xbox_token = xsts_authorize(sisu_authorize, &token.token, &key).await?; let minecraft_token = minecraft_token(xbox_token).await?; @@ -185,7 +202,8 @@ impl MinecraftState { username: profile_name, access_token: minecraft_token.access_token, refresh_token: oauth_token.refresh_token, - #[allow(deprecated)] expires: Utc::now() + chrono::TimeDelta::seconds(oauth_token.expires_in as i64), + #[allow(deprecated)] + expires: Utc::now() + chrono::TimeDelta::seconds(oauth_token.expires_in as i64), }; self.users.insert(val.id, val.clone()); @@ -315,7 +333,9 @@ pub async fn device_token(key: &DeviceKey) -> Result Result Result Result Result Result( - request_fn: impl Fn() -> F, -) -> Result +async fn auth_retry(request_fn: impl Fn() -> F) -> Result where F: std::future::Future>, { @@ -613,7 +690,9 @@ where Err(err) => { if err.is_connect() || err.is_timeout() { if i < RETRY_COUNT - 1 { - tracing::debug!("request failed due to a connection error, retrying request..."); + tracing::debug!( + "request failed due to a connection error, retrying request..." + ); tokio::time::sleep(RETRY_WAIT).await; resp = request_fn().await; } else { @@ -638,15 +717,15 @@ fn device_key() -> Result { id, key: signing_key, x: base64::prelude::BASE64_STANDARD_NO_PAD.encode( - encoded_point.x().ok_or_else(|| { - MinecraftAuthError::PublicKeyReading - })?, + encoded_point + .x() + .ok_or_else(|| MinecraftAuthError::PublicKeyReading)?, ), y: base64::prelude::BASE64_STANDARD_NO_PAD.encode( - encoded_point.y().ok_or_else(|| { - MinecraftAuthError::PublicKeyReading - })?, - ) + encoded_point + .y() + .ok_or_else(|| MinecraftAuthError::PublicKeyReading)?, + ), }) } @@ -658,52 +737,53 @@ async fn send_signed_request( key: &DeviceKey, step: MinecraftAuthStep, ) -> Result<(reqwest::header::HeaderMap, T), MinecraftAuthError> { - let auth = authorization.clone().map_or(Vec::new(), |v| v.as_bytes().to_vec()); - let body = serde_json::to_vec(&raw_body).map_err(|source| { - MinecraftAuthError::SerializeError { step, source } - })?; + let auth = authorization + .clone() + .map_or(Vec::new(), |v| v.as_bytes().to_vec()); + let body = serde_json::to_vec(&raw_body) + .map_err(|source| MinecraftAuthError::SerializeError { step, source })?; let time: u128 = { ((Utc::now().timestamp() as u128) + 11644473600) * 10000000 }; - + use byteorder::WriteBytesExt; let mut buffer = Vec::new(); - buffer.write_u32::(1).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; - buffer.write_u8(0).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; - buffer.write_u64::(time as u64).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; - buffer.write_u8(0).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; + buffer + .write_u32::(1) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; + buffer + .write_u8(0) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; + buffer + .write_u64::(time as u64) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; + buffer + .write_u8(0) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; buffer.extend_from_slice("POST".as_bytes()); - buffer.write_u8(0).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; + buffer + .write_u8(0) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; buffer.extend_from_slice(url_path.as_bytes()); - buffer.write_u8(0).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; + buffer + .write_u8(0) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; buffer.extend_from_slice(&auth); - buffer.write_u8(0).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; + buffer + .write_u8(0) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; buffer.extend_from_slice(&body); - buffer.write_u8(0).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; - + buffer + .write_u8(0) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; + let ecdsa_sig: p256::ecdsa::Signature = key.key.sign(&buffer); let mut sig_buffer = Vec::new(); - sig_buffer.write_i32::(1).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; - sig_buffer.write_u64::(time as u64).map_err(|source| { - MinecraftAuthError::SigningError { step, source } - })?; + sig_buffer + .write_i32::(1) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; + sig_buffer + .write_u64::(time as u64) + .map_err(|source| MinecraftAuthError::SigningError { step, source })?; sig_buffer.extend_from_slice(&ecdsa_sig.r().to_bytes()); sig_buffer.extend_from_slice(&ecdsa_sig.s().to_bytes()); @@ -725,18 +805,24 @@ async fn send_signed_request( } request.body(body.clone()).send() - }).await + }) + .await .map_err(|source| MinecraftAuthError::RequestError { step, source })?; let status = res.status(); let headers = res.headers().clone(); - let body = res.text().await.map_err(|source| { - MinecraftAuthError::RequestError { step, source } - })?; + let body = res + .text() + .await + .map_err(|source| MinecraftAuthError::RequestError { step, source })?; - let body = serde_json::from_str(&body).map_err(|source| { - MinecraftAuthError::DeserializeError { step, raw: body, source, status_code: status } - })?; + let body = + serde_json::from_str(&body).map_err(|source| MinecraftAuthError::DeserializeError { + step, + raw: body, + source, + status_code: status, + })?; Ok((headers, body)) } @@ -750,15 +836,15 @@ fn generate_oauth_challenge() -> String { /// An ordered list of all MSA authentication steps. #[derive(Debug, Clone, Copy)] pub enum MinecraftAuthStep { - DeviceToken, - SisuAuthenicate, - OAuthToken, - RefreshOAuthToken, - SisuAuthorize, - XstsAuthorize, - MinecraftToken, - MinecraftEntitlements, - MinecraftProfile, + DeviceToken, + SisuAuthenicate, + OAuthToken, + RefreshOAuthToken, + SisuAuthorize, + XstsAuthorize, + MinecraftToken, + MinecraftEntitlements, + MinecraftProfile, } /// Wrapper around all `Error`s that can occur during the Microsoft authentication process. @@ -769,13 +855,31 @@ pub enum MinecraftAuthError { #[error("failed to serialize private key using PKCS8: {0}")] PKCS8Error(#[from] p256::pkcs8::Error), #[error("failed to serialize JSON during MSA step {step:?}: {source}")] - SerializeError { step: MinecraftAuthStep, #[source] source: serde_json::Error }, + SerializeError { + step: MinecraftAuthStep, + #[source] + source: serde_json::Error, + }, #[error("failed to deserialize JSON during MSA step {step:?}: {source}! status code {status_code} - body: {raw}")] - DeserializeError { step: MinecraftAuthStep, raw: String, #[source] source: serde_json::Error, status_code: reqwest::StatusCode }, + DeserializeError { + step: MinecraftAuthStep, + raw: String, + #[source] + source: serde_json::Error, + status_code: reqwest::StatusCode, + }, #[error("failed to request using HTTP during MSA step {step:?}: {source}")] - RequestError { step: MinecraftAuthStep, #[source] source: reqwest::Error }, + RequestError { + step: MinecraftAuthStep, + #[source] + source: reqwest::Error, + }, #[error("failed to create signed buffer during MSA step {step:?}: {source}")] - SigningError { step: MinecraftAuthStep, #[source] source: std::io::Error }, + SigningError { + step: MinecraftAuthStep, + #[source] + source: std::io::Error, + }, #[error("failed to read user hash")] HashError, #[error("failed to read user xbox session ID")] diff --git a/core/src/store/mod.rs b/core/src/store/mod.rs index 89014fff..a90fad57 100644 --- a/core/src/store/mod.rs +++ b/core/src/store/mod.rs @@ -1,5 +1,5 @@ //! **OneLauncher Store** -//! +//! //! Core state and storage management for the launcher, managing all states. use std::sync::Arc; @@ -7,16 +7,11 @@ use std::time::Duration; use notify::RecommendedWatcher; use notify_debouncer_mini::Debouncer; -use tokio::sync::OnceCell; -use tokio::sync::RwLock; -use tokio::sync::Semaphore; +use tokio::sync::{OnceCell, RwLock, Semaphore}; -use crate::proxy::send::init_ingress_internal; -use crate::proxy::send::send_ingress; -use crate::proxy::send::send_offline; +use crate::proxy::send::{init_ingress_internal, send_ingress, send_offline}; use crate::utils::http; -use crate::utils::http::FetchSemaphore; -use crate::utils::http::IoSemaphore; +use crate::utils::http::{FetchSemaphore, IoSemaphore}; mod dirs; pub use self::dirs::*; @@ -58,218 +53,230 @@ static LAUNCHER_STATE: OnceCell> = 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, - /// Handles core launcher directory and files - pub directories: Directories, + /// Handles a boolean to check if the launcher is connected to the internet + pub offline: RwLock, + /// 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, - /// 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, + /// 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, + /// 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, - /// Handles launcher metadata via [`interpulse`] - pub metadata: RwLock, - /// Handles launcher configuration and settings - pub settings: RwLock, - /// Handles minecraft auth flows and users - pub users: RwLock, - /// Handles processes - pub processor: RwLock, - /// Handles OneConfig integration with [`async-tungstenite`] - pub oneconfig: RwLock>, - /// Handles secure credential management with [`iota_stronghold`]. - pub credentials: RwLock>, - /// Handles clusters - pub(crate) clusters: RwLock, - /// Handles internal ingress processes - pub(crate) ingress_processor: RwLock, - /// Handles file system watching for cluster manager - pub(crate) watcher: RwLock>, - /// Handles Discord rich prescence - pub discord_rpc: DiscordRPC, + /// Handles launcher metadata via [`interpulse`] + pub metadata: RwLock, + /// Handles launcher configuration and settings + pub settings: RwLock, + /// Handles minecraft auth flows and users + pub users: RwLock, + /// Handles processes + pub processor: RwLock, + /// Handles OneConfig integration with [`async-tungstenite`] + pub oneconfig: RwLock>, + /// Handles secure credential management with [`iota_stronghold`]. + pub credentials: RwLock>, + /// Handles clusters + pub(crate) clusters: RwLock, + /// Handles internal ingress processes + pub(crate) ingress_processor: RwLock, + /// Handles file system watching for cluster manager + pub(crate) watcher: RwLock>, + /// 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>> { - Ok(Arc::new(LAUNCHER_STATE.get_or_try_init(Self::initialize).await?.read().await)) - } + /// Get the current global launcher state (or initialize it) + pub async fn get() -> crate::Result>> { + 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> { - Ok(LAUNCHER_STATE.get_or_try_init(Self::initialize).await?.write().await) - } + /// Get and lock writing to the current global launcher state (or initalize it) + pub async fn get_and_write() -> crate::Result> { + Ok(LAUNCHER_STATE + .get_or_try_init(Self::initialize) + .await? + .write() + .await) + } - /// Initializes the core OneLauncher state fully. - #[tracing::instrument] - #[onelauncher_debug::debugger] - async fn initialize() -> crate::Result> { - let ingress = init_ingress_internal( - crate::IngressType::Initialize, - 100.0, - "initializing state", - ).await?; + /// Initializes the core OneLauncher state fully. + #[tracing::instrument] + #[onelauncher_debug::debugger] + async fn initialize() -> crate::Result> { + 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().await?; - 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 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().await?; + 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; + let is_offline = !http::check_internet_connection(3).await; - let metadata_in = Metadata::initialize(&directories, !is_offline, &io_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, - }?; + let metadata_in = Metadata::initialize(&directories, !is_offline, &io_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, + }?; - let ingress_processor = IngressProcessor::new(); - let discord_rpc = DiscordRPC::initialize(is_offline).await?; - if !settings.disable_discord && !is_offline { - let _ = discord_rpc.apply_activity("Idling...", true).await; - } + let ingress_processor = IngressProcessor::new(); + let discord_rpc = DiscordRPC::initialize(is_offline).await?; + if !settings.disable_discord && !is_offline { + let _ = discord_rpc.apply_activity("Idling...", true).await; + } - let processor = Processor::new(); + let processor = Processor::new(); - Self::status_loop(); - send_ingress(&ingress, 10.0, None).await?; + Self::status_loop(); + send_ingress(&ingress, 10.0, None).await?; - Ok::, 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), - ingress_processor: RwLock::new(ingress_processor), - watcher: RwLock::new(watcher), - discord_rpc, - })) - } + Ok::, 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), + 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; - } + /// 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; - } - }); - } + 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 { - if let Ok(state) = crate::State::get().await { - if !*state.offline.read().await { - let version_up = Clusters::update_versions(); - let meta_up = Metadata::update(); - let package_up = Clusters::update_packages(); + /// Updates all data if we are connected to the internet. + pub fn update() { + tokio::task::spawn(async { + if let Ok(state) = crate::State::get().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); - } - } - }); - } + let _ = tokio::join!(version_up, meta_up, package_up); + } + } + }); + } - /// Synchronizes data that can change outside of our control. - #[tracing::instrument] - #[onelauncher_debug::debugger] - 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? - }; + /// Synchronizes data that can change outside of our control. + #[tracing::instrument] + #[onelauncher_debug::debugger] + 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); + let sync_clusters = async { + let state = Arc::clone(&state); - tokio::spawn(async move { - let clusters = state.clusters.read().await; + tokio::spawn(async move { + let clusters = state.clusters.read().await; - clusters.sync().await?; - Ok::<_, crate::Error>(()) - }).await? - }; + clusters.sync().await?; + Ok::<_, crate::Error>(()) + }) + .await? + }; - tokio::try_join!(sync_settings, sync_clusters)?; - Ok(()) - } + 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; + /// 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); - } + 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; + /// 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); - } + 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(()); - } + /// 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(()) - } + 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() - } + /// Determine if the global state is initalized or not (wrapper) + pub fn initalized() -> bool { + LAUNCHER_STATE.initialized() + } } diff --git a/core/src/store/oneconfig.rs b/core/src/store/oneconfig.rs index bdc1d764..48bff411 100644 --- a/core/src/store/oneconfig.rs +++ b/core/src/store/oneconfig.rs @@ -1,34 +1,33 @@ //! OneConfig state using [`async_tungstenite`] to communicate to the OneConfig server +use super::ClusterPath; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use super::ClusterPath; /// Wrapper over an [`async_tungstenite::WebSocketStream`] to connect to the OneConfig server. pub struct OneConfig { - /// The public tokio socket connection. - socket: async_tungstenite::WebSocketStream< - async_tungstenite::tokio::ConnectStream, - >, + /// The public tokio socket connection. + socket: async_tungstenite::WebSocketStream, } /// A WebSocket packet while updating a mod to display a GUI in game. #[derive(Serialize, Deserialize, Debug)] pub struct ModUpdatePacket { - /// The ID of the packet. - id: Uuid, - /// The ID of the mod being updated. - mod_id: Uuid, - /// The [`ClusterPath`] associated with this event. - cluster_path: ClusterPath, + /// The ID of the packet. + id: Uuid, + /// The ID of the mod being updated. + mod_id: Uuid, + /// The [`ClusterPath`] associated with this event. + cluster_path: ClusterPath, } impl OneConfig { - /// Initializes a OneConfig socket connection on localhost port `4023`. - pub async fn new(cluster: ClusterPath) -> crate::Result { - let (socket, _) = async_tungstenite::tokio::connect_async(format!( - "wss://localhost:4023/oneconfig/ws?cluster={cluster}" - )).await?; - Ok(Self { socket }) - } + /// Initializes a OneConfig socket connection on localhost port `4023`. + pub async fn new(cluster: ClusterPath) -> crate::Result { + let (socket, _) = async_tungstenite::tokio::connect_async(format!( + "wss://localhost:4023/oneconfig/ws?cluster={cluster}" + )) + .await?; + Ok(Self { socket }) + } } diff --git a/core/src/store/package.rs b/core/src/store/package.rs index a0c143c0..43403482 100644 --- a/core/src/store/package.rs +++ b/core/src/store/package.rs @@ -1,17 +1,19 @@ //! Handlers for Mod metadata that can be displayed in a GUI mod list or exported as a mod pack -use std::{collections::HashMap, path::{Path, PathBuf}}; +use async_zip::tokio::read::fs::ZipFileReader; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use async_zip::tokio::read::fs::ZipFileReader; use sha2::Digest; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use tokio::io::AsyncReadExt; -use crate::{prelude::IOError, utils::http::{write_icon, FetchSemaphore, IoSemaphore}}; +use crate::prelude::IOError; +use crate::utils::http::{write_icon, FetchSemaphore, IoSemaphore}; use super::{Cluster, PackagePath}; -// TODO: move all of this to prisma, it would be perfect for it +// TODO: move all of this to prisma, it would be perfect for it // TODO: Curseforge, Modrinth, SkyClient integration /// Creates [`Package`] data for a given [`Cluster`] from on-device files and APIs. @@ -19,269 +21,292 @@ use super::{Cluster, PackagePath}; #[tracing::instrument(skip(paths, cluster, io_semaphore, fetch_semaphore))] #[onelauncher_debug::debugger] pub async fn generate_context( - cluster: Cluster, - paths: Vec, - cache_path: PathBuf, - io_semaphore: &IoSemaphore, - fetch_semaphore: &FetchSemaphore, - // credentials: &Credentials, + cluster: Cluster, + paths: Vec, + cache_path: PathBuf, + io_semaphore: &IoSemaphore, + fetch_semaphore: &FetchSemaphore, + // credentials: &Credentials, ) -> crate::Result> { - let mut hashes = HashMap::new(); + let mut hashes = HashMap::new(); - for path in paths { - if !path.exists() { continue; } - if let Some(ext) = path.extension() { if ext == "txt" { continue; } } - let mut file = tokio::fs::File::open(path.clone()).await.map_err(|e| IOError::with_path(e, &path))?; - let mut buffer = [0u8; 4096]; - let mut sha512 = sha2::Sha512::new(); + for path in paths { + if !path.exists() { + continue; + } + if let Some(ext) = path.extension() { + if ext == "txt" { + continue; + } + } + let mut file = tokio::fs::File::open(path.clone()) + .await + .map_err(|e| IOError::with_path(e, &path))?; + let mut buffer = [0u8; 4096]; + let mut sha512 = sha2::Sha512::new(); - loop { - let read = file.read(&mut buffer).await.map_err(IOError::from)?; - if read == 0 { break; } - sha512.update(&buffer[..read]); - } + loop { + let read = file.read(&mut buffer).await.map_err(IOError::from)?; + if read == 0 { + break; + } + sha512.update(&buffer[..read]); + } - let hash = format!("{:x}", sha512.finalize()); - hashes.insert(hash, path.clone()); - } + let hash = format!("{:x}", sha512.finalize()); + hashes.insert(hash, path.clone()); + } - let mut result = HashMap::new(); - let mut stream = tokio_stream::iter(result_packages); - while let Some((k, v)) = stream.next().await { - let k = PackagePath::from_path(k).await?; - result.insert(k, v); - } + let mut result = HashMap::new(); + let mut stream = tokio_stream::iter(result_packages); + while let Some((k, v)) = stream.next().await { + let k = PackagePath::from_path(k).await?; + result.insert(k, v); + } - Ok(result) + Ok(result) } /// Represents types of packages handled by the launcher. #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum PackageType { - /// represents a mod jarfile - Mod, - /// represents a datapack file - DataPack, - /// represents a resourcepack file - Resource, - /// represents a shaderpack file - Shader, + /// represents a mod jarfile + Mod, + /// represents a datapack file + DataPack, + /// represents a resourcepack file + Resource, + /// represents a shaderpack file + Shader, } impl PackageType { - /// attempt to get a [`PackageType`] from a loader string. - pub fn from_loader(loaders: Vec) -> Option { - if loaders.iter().any(|x| ["fabric", "forge", "quilt"].contains(&&**x)) { - Some(PackageType::Mod) - } else if loaders.iter().any(|x| x == "datapack") { - Some(PackageType::DataPack) - } else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) { - Some(PackageType::Shader) - } else if loaders.iter().any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x)) { - Some(PackageType::Resource) - } else { - None - } - } + /// attempt to get a [`PackageType`] from a loader string. + pub fn from_loader(loaders: Vec) -> Option { + if loaders + .iter() + .any(|x| ["fabric", "forge", "quilt"].contains(&&**x)) + { + Some(PackageType::Mod) + } else if loaders.iter().any(|x| x == "datapack") { + Some(PackageType::DataPack) + } else if loaders.iter().any(|x| ["iris", "optifine"].contains(&&**x)) { + Some(PackageType::Shader) + } else if loaders + .iter() + .any(|x| ["vanilla", "canvas", "minecraft"].contains(&&**x)) + { + Some(PackageType::Resource) + } else { + None + } + } - pub fn from_parent(path: PathBuf) -> Option { - let path = path.parent()?.file_name()?; - match path.to_str()? { - "mods" => Some(PackageType::Mod), - "datapacks" => Some(PackageType::DataPack), - "resourcepacks" => Some(PackageType::Resource), - "shaderpacks" => Some(PackageType::Shader), - _ => None, - } - } + pub fn from_parent(path: PathBuf) -> Option { + let path = path.parent()?.file_name()?; + match path.to_str()? { + "mods" => Some(PackageType::Mod), + "datapacks" => Some(PackageType::DataPack), + "resourcepacks" => Some(PackageType::Resource), + "shaderpacks" => Some(PackageType::Shader), + _ => None, + } + } - pub fn get_name(&self) -> &'static str { - match self { - PackageType::Mod => "mod", - PackageType::DataPack => "datapack", - PackageType::Resource => "resourcepack", - PackageType::Shader => "shaderpack", - } - } + pub fn get_name(&self) -> &'static str { + match self { + PackageType::Mod => "mod", + PackageType::DataPack => "datapack", + PackageType::Resource => "resourcepack", + PackageType::Shader => "shaderpack", + } + } - pub fn get_folder(&self) -> &'static str { - match self { - PackageType::Mod => "mods", - PackageType::DataPack => "datapacks", - PackageType::Resource => "resourcepacks", - PackageType::Shader => "shaderpacks", - } - } + pub fn get_folder(&self) -> &'static str { + match self { + PackageType::Mod => "mods", + PackageType::DataPack => "datapacks", + PackageType::Resource => "resourcepacks", + PackageType::Shader => "shaderpacks", + } + } } /// A struct that represents a Package. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Package { - pub sha512: String, - pub meta: PackageMetadata, - pub file_name: String, - pub disabled: bool, + pub sha512: String, + pub meta: PackageMetadata, + pub file_name: String, + pub disabled: bool, } /// Metadata that represents a [`Package`]. #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum PackageMetadata { - Managed { - package: Box, - version: Box, - users: Box, - update: Option>, - incompatible: bool, - }, - Mapped { - title: Option, - description: Option, - authors: Vec, - version: Option, - icon: Option, - package_type: Option, - }, - Unknown, + Managed { + package: Box, + version: Box, + users: Box, + update: Option>, + incompatible: bool, + }, + Mapped { + title: Option, + description: Option, + authors: Vec, + version: Option, + icon: Option, + package_type: Option, + }, + Unknown, } /// Universal metadata for any managed package from a Mod distribution platform. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedPackage { - // Core Metadata + // Core Metadata + pub id: String, + pub uid: Option, + pub package_type: String, + pub title: String, + pub description: String, + pub main: String, + pub versions: Vec, + pub game_versions: Vec, + pub loaders: Vec, + pub icon_url: Option, - pub id: String, - pub uid: Option, - pub package_type: String, - pub title: String, - pub description: String, - pub main: String, - pub versions: Vec, - pub game_versions: Vec, - pub loaders: Vec, - pub icon_url: Option, - - pub created: DateTime, - pub updated: DateTime, - pub client: PackageSide, - pub server: PackageSide, - pub downloads: u32, - pub followers: u32, - pub categories: Vec, - pub optional_categories: Option>, + pub created: DateTime, + pub updated: DateTime, + pub client: PackageSide, + pub server: PackageSide, + pub downloads: u32, + pub followers: u32, + pub categories: Vec, + pub optional_categories: Option>, } /// Universal managed package version of a package. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedVersion { - pub id: String, - pub package_id: String, - pub author: String, - pub name: String, - - pub featured: bool, - pub version_id: String, - pub changelog: String, - pub changelog_url: Option, + pub id: String, + pub package_id: String, + pub author: String, + pub name: String, - pub published: DateTime, - pub downloads: u32, - pub version_type: String, + pub featured: bool, + pub version_id: String, + pub changelog: String, + pub changelog_url: Option, - pub files: Vec, - pub deps: Vec, - pub game_versions: Vec, - pub loaders: Vec, + pub published: DateTime, + pub downloads: u32, + pub version_type: String, + + pub files: Vec, + pub deps: Vec, + pub game_versions: Vec, + pub loaders: Vec, } /// Universal interface for managed package files. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedVersionFile { - pub url: String, - pub file_name: String, - pub primary: bool, + pub url: String, + pub file_name: String, + pub primary: bool, - pub size: u32, - pub file_type: Option, - pub hashes: HashMap, + pub size: u32, + pub file_type: Option, + pub hashes: HashMap, } /// Universal interface for managed package dependencies. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedDependency { - pub version_id: Option, - pub package_id: Option, - pub file_name: Option, - pub dependency_type: PackageDependency, + pub version_id: Option, + pub package_id: Option, + pub file_name: Option, + pub dependency_type: PackageDependency, } /// Universal interface for managed package authors and users. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ManagedUser { - pub id: String, - pub role: String, - pub username: String, - pub name: Option, - pub avatar: Option, - pub description: Option, - pub created: DateTime, + pub id: String, + pub role: String, + pub username: String, + pub name: Option, + pub avatar: Option, + pub description: Option, + pub created: DateTime, } -/// The type of a [`ManagedDependency`]. +/// The type of a [`ManagedDependency`]. #[derive(Serialize, Deserialize, Clone, Debug)] pub enum PackageDependency { - Required, - Optional, - Incompatible, - Embedded, + Required, + Optional, + Incompatible, + Embedded, } /// The Client/Server side type of a [`Package`]. #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum PackageSide { - Required, - Optional, - Unsupported, - Unknown, + Required, + Optional, + Unsupported, + Unknown, } /// The file type of a [`Package`]. #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub enum PackageFile { - RequiredPack, - OptionalPack, - Unknown, + RequiredPack, + OptionalPack, + Unknown, } #[tracing::instrument(skip(io_semaphore))] #[onelauncher_debug::debugger] async fn read_icon( - icon_path: Option, - cache_path: &Path, - path: &PathBuf, - io_semaphore: &IoSemaphore, + icon_path: Option, + cache_path: &Path, + path: &PathBuf, + io_semaphore: &IoSemaphore, ) -> crate::Result> { - if let Some(icon_path) = icon_path { - let zip_reader = ZipFileReader::new(path).await; - if let Ok(zip_reader) = zip_reader { - let zip_idx_res = zip_reader.file().entries().iter().position(|f| { - f.filename().as_str().unwrap_or_default() == icon_path - }); + if let Some(icon_path) = icon_path { + let zip_reader = ZipFileReader::new(path).await; + if let Ok(zip_reader) = zip_reader { + let zip_idx_res = zip_reader + .file() + .entries() + .iter() + .position(|f| f.filename().as_str().unwrap_or_default() == icon_path); - if let Some(zip_idx) = zip_idx_res { - let mut bytes = Vec::new(); - if zip_reader.reader_with_entry(zip_idx).await?.read_to_end_checked(&mut bytes).await.is_ok() { - let bytes = bytes::Bytes::from(bytes); - let path = write_icon(&icon_path, cache_path, bytes, io_semaphore).await?; + if let Some(zip_idx) = zip_idx_res { + let mut bytes = Vec::new(); + if zip_reader + .reader_with_entry(zip_idx) + .await? + .read_to_end_checked(&mut bytes) + .await + .is_ok() + { + let bytes = bytes::Bytes::from(bytes); + let path = write_icon(&icon_path, cache_path, bytes, io_semaphore).await?; - return Ok(Some(path)); - } - } - } - } + return Ok(Some(path)); + } + } + } + } - Ok(None) + Ok(None) } diff --git a/core/src/store/processor.rs b/core/src/store/processor.rs index e9685a86..ae6fc19d 100644 --- a/core/src/store/processor.rs +++ b/core/src/store/processor.rs @@ -1,19 +1,20 @@ //! Handles child processes in a unified API -use std::{collections::HashMap, sync::Arc}; -use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; -use tokio::task::JoinHandle; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; use tokio::process::{Child, Command}; use tokio::sync::RwLock; +use tokio::task::JoinHandle; use uuid::Uuid; +use crate::api::cluster; use crate::proxy::send::send_process; +use crate::proxy::ProcessPayloadType; use crate::utils::http::read_json; use crate::utils::io::IOError; -use crate::proxy::ProcessPayloadType; use crate::State; -use crate::api::cluster; use super::{Cluster, ClusterPath}; @@ -25,593 +26,666 @@ pub struct Processor(HashMap>>); /// Manager for safe processes like ingress feeds which need to be safely shut down. pub struct IngressProcessor { - /// A vector of ingress feed uuids. - pub ingress_feeds: Vec, + /// A vector of ingress feed uuids. + pub ingress_feeds: Vec, } /// A type of Ingress safety process. #[derive(Debug, Copy, Clone)] pub enum IngressProcessType { - /// A single Ingress feed. - IngressFeed, - // Writes, - // Reads, - // Fetch, + /// A single Ingress feed. + IngressFeed, + // Writes, + // Reads, + // Fetch, } impl IngressProcessor { - /// Initialize a new ingresss process manager. - pub fn new() -> Self { - Self { - ingress_feeds: Vec::new() - } - } + /// Initialize a new ingresss process manager. + pub fn new() -> Self { + Self { + ingress_feeds: Vec::new(), + } + } - /// Add an ingress safety process to the HashMap. - pub async fn add_ingress( - r#type: IngressProcessType, - uuid: Uuid, - ) -> crate::Result { - let state = State::get().await?; - let mut ingress_processor = state.ingress_processor.write().await; - match r#type { - IngressProcessType::IngressFeed => { - ingress_processor.ingress_feeds.push(uuid); - } - } + /// Add an ingress safety process to the HashMap. + pub async fn add_ingress(r#type: IngressProcessType, uuid: Uuid) -> crate::Result { + let state = State::get().await?; + let mut ingress_processor = state.ingress_processor.write().await; + match r#type { + IngressProcessType::IngressFeed => { + ingress_processor.ingress_feeds.push(uuid); + } + } - Ok(uuid) - } + Ok(uuid) + } - /// Complete and retain an ingress safety process from the HashMap of ingress feeds. - pub async fn finish( - r#type: IngressProcessType, - uuid: Uuid, - ) -> crate::Result<()> { - let state = State::get().await?; - let mut ingress_processor = state.ingress_processor.write().await; - match r#type { - IngressProcessType::IngressFeed => { - ingress_processor.ingress_feeds.retain(|f| *f != uuid); - } - } - Ok(()) - } + /// Complete and retain an ingress safety process from the HashMap of ingress feeds. + pub async fn finish(r#type: IngressProcessType, uuid: Uuid) -> crate::Result<()> { + let state = State::get().await?; + let mut ingress_processor = state.ingress_processor.write().await; + match r#type { + IngressProcessType::IngressFeed => { + ingress_processor.ingress_feeds.retain(|f| *f != uuid); + } + } + Ok(()) + } - /// Finish and check if an ingress safety processes ingress feed is empty and finished. - pub async fn finished( - r#type: IngressProcessType, - ) -> crate::Result { - let state = State::get().await?; - let ingress_processor = state.ingress_processor.read().await; - match r#type { - IngressProcessType::IngressFeed => { - if ingress_processor.ingress_feeds.is_empty() { - return Ok(true) - } - } - } + /// Finish and check if an ingress safety processes ingress feed is empty and finished. + pub async fn finished(r#type: IngressProcessType) -> crate::Result { + let state = State::get().await?; + let ingress_processor = state.ingress_processor.read().await; + match r#type { + IngressProcessType::IngressFeed => { + if ingress_processor.ingress_feeds.is_empty() { + return Ok(true); + } + } + } - Ok(false) - } + Ok(false) + } } /// A child process type. #[derive(Debug)] pub enum ChildType { - /// A tokio-managed child process. - ChildProcess(Child), - /// A restored child from a process cache that needs to be re-registered. - RescuedChild(u32), + /// A tokio-managed child process. + ChildProcess(Child), + /// A restored child from a process cache that needs to be re-registered. + RescuedChild(u32), } /// A structure representing a cache of a running process. #[derive(Serialize, Deserialize, Debug)] pub struct ProcessorCache { - /// The PID of a process. - pub pid: u32, - /// The stored UUID of a process. - pub uuid: Uuid, - /// The time a process started. - pub start_time: u64, - /// The name of a process. - pub name: String, - /// The executable file running the process. - pub exe: String, - /// The associated [`ClusterPath`] to a process. - pub cluster_path: ClusterPath, - /// A post hook to be run once a process has completed. - pub post: Option, + /// The PID of a process. + pub pid: u32, + /// The stored UUID of a process. + pub uuid: Uuid, + /// The time a process started. + pub start_time: u64, + /// The name of a process. + pub name: String, + /// The executable file running the process. + pub exe: String, + /// The associated [`ClusterPath`] to a process. + pub cluster_path: ClusterPath, + /// A post hook to be run once a process has completed. + pub post: Option, } impl ChildType { - /// Get the PID of a specific ChildType process. - pub fn id(&self) -> Option { - match self { - ChildType::ChildProcess(child) => child.id(), - ChildType::RescuedChild(pid) => Some(*pid), - } - } + /// Get the PID of a specific ChildType process. + pub fn id(&self) -> Option { + match self { + ChildType::ChildProcess(child) => child.id(), + ChildType::RescuedChild(pid) => Some(*pid), + } + } - /// Store a cache of a specific ChildType. - pub async fn cache(&self, uuid: Uuid, cluster_path: ClusterPath, post: Option) -> crate::Result<()> { - let pid = match self { - ChildType::ChildProcess(child) => child.id().unwrap_or(0), - ChildType::RescuedChild(pid) => *pid, - }; + /// Store a cache of a specific ChildType. + pub async fn cache( + &self, + uuid: Uuid, + cluster_path: ClusterPath, + post: Option, + ) -> crate::Result<()> { + let pid = match self { + ChildType::ChildProcess(child) => child.id().unwrap_or(0), + ChildType::RescuedChild(pid) => *pid, + }; - let state = crate::State::get().await?; - let mut system = sysinfo::System::new(); + let state = crate::State::get().await?; + let mut system = sysinfo::System::new(); - system.refresh_processes(); - let process = system.process(sysinfo::Pid::from_u32(pid)).ok_or_else(|| { - anyhow::anyhow!("could not find process {}", pid) - })?; - let start_time = process.start_time(); - let name = process.name().to_string(); - let Some(path) = process.exe() else { - return Err(anyhow::anyhow!("cached process {} has no path", pid).into()) - }; + system.refresh_processes(); + let process = system + .process(sysinfo::Pid::from_u32(pid)) + .ok_or_else(|| anyhow::anyhow!("could not find process {}", pid))?; + let start_time = process.start_time(); + let name = process.name().to_string(); + let Some(path) = process.exe() else { + return Err(anyhow::anyhow!("cached process {} has no path", pid).into()); + }; - let exe = path.to_string_lossy().to_string(); - let cached_proccess = ProcessorCache { - pid, - uuid, - start_time, - name, - exe, - cluster_path, - post, - }; + let exe = path.to_string_lossy().to_string(); + let cached_proccess = ProcessorCache { + pid, + uuid, + start_time, + name, + exe, + cluster_path, + post, + }; - let path = state - .directories - .caches_dir() - .await - .join(PROCESSOR_JSON); - let mut caches = if let Ok(proc_json) = - read_json::>( - &path, - &state.io_semaphore, - ).await - { - proc_json - } else { - HashMap::new() - }; + let path = state.directories.caches_dir().await.join(PROCESSOR_JSON); + let mut caches = if let Ok(proc_json) = + read_json::>(&path, &state.io_semaphore).await + { + proc_json + } else { + HashMap::new() + }; - caches.insert(uuid, cached_proccess); - crate::utils::http::write( - &path, - &serde_json::to_vec(&caches)?, - &state.io_semaphore, - ).await?; + caches.insert(uuid, cached_proccess); + crate::utils::http::write(&path, &serde_json::to_vec(&caches)?, &state.io_semaphore) + .await?; - Ok(()) - } + Ok(()) + } - /// Remove a child from the global store of processes. - pub async fn remove(&self, uuid: Uuid) -> crate::Result<()> { - let state = crate::State::get().await?; + /// Remove a child from the global store of processes. + pub async fn remove(&self, uuid: Uuid) -> crate::Result<()> { + let state = crate::State::get().await?; - let path = state - .directories - .caches_dir() - .await - .join(PROCESSOR_JSON); - let mut caches = if let Ok(proc_json) = - read_json::>( - &path, - &state.io_semaphore, - ).await - { - proc_json - } else { - HashMap::new() - }; - caches.remove(&uuid); - crate::utils::http::write( - &path, - &serde_json::to_vec(&caches)?, - &state.io_semaphore, - ).await?; + let path = state.directories.caches_dir().await.join(PROCESSOR_JSON); + let mut caches = if let Ok(proc_json) = + read_json::>(&path, &state.io_semaphore).await + { + proc_json + } else { + HashMap::new() + }; + caches.remove(&uuid); + crate::utils::http::write(&path, &serde_json::to_vec(&caches)?, &state.io_semaphore) + .await?; - Ok(()) - } + Ok(()) + } - /// Kill a process in the global store of processes. - pub async fn kill(&mut self) -> crate::Result<()> { - match self { - ChildType::ChildProcess(child) => { - Ok(child.kill().await.map_err(IOError::from)?) - } - ChildType::RescuedChild(pid) => { - let mut system = sysinfo::System::new(); - if system.refresh_process(sysinfo::Pid::from_u32(*pid)) { - let process = system.process(sysinfo::Pid::from_u32(*pid)); - if let Some(process) = process { - process.kill(); - } - } - Ok(()) - } - } - } + /// Kill a process in the global store of processes. + pub async fn kill(&mut self) -> crate::Result<()> { + match self { + ChildType::ChildProcess(child) => Ok(child.kill().await.map_err(IOError::from)?), + ChildType::RescuedChild(pid) => { + let mut system = sysinfo::System::new(); + if system.refresh_process(sysinfo::Pid::from_u32(*pid)) { + let process = system.process(sysinfo::Pid::from_u32(*pid)); + if let Some(process) = process { + process.kill(); + } + } + Ok(()) + } + } + } - /// Wait for a process to complete. - pub async fn try_wait(&mut self) -> crate::Result> { - match self { - ChildType::ChildProcess(child) => Ok(child - .try_wait() - .map_err(IOError::from)? - .map(|x| x.code().unwrap_or(0))), - ChildType::RescuedChild(pid) => { - let mut system = sysinfo::System::new(); - if !system.refresh_process(sysinfo::Pid::from_u32(*pid)) { - return Ok(Some(0)) - } + /// Wait for a process to complete. + pub async fn try_wait(&mut self) -> crate::Result> { + match self { + ChildType::ChildProcess(child) => Ok(child + .try_wait() + .map_err(IOError::from)? + .map(|x| x.code().unwrap_or(0))), + ChildType::RescuedChild(pid) => { + let mut system = sysinfo::System::new(); + if !system.refresh_process(sysinfo::Pid::from_u32(*pid)) { + return Ok(Some(0)); + } - let process = system.process(sysinfo::Pid::from_u32(*pid)); - if let Some(process) = process { - if process.status() == sysinfo::ProcessStatus::Run { - Ok(None) - } else { - Ok(Some(0)) - } - } else { - Ok(Some(0)) - } - } - } - } + let process = system.process(sysinfo::Pid::from_u32(*pid)); + if let Some(process) = process { + if process.status() == sysinfo::ProcessStatus::Run { + Ok(None) + } else { + Ok(Some(0)) + } + } else { + Ok(Some(0)) + } + } + } + } } /// A [`Processor`] child process. #[derive(Debug)] pub struct ProcessorChild { - /// The uuid of a process. - pub uuid: Uuid, - /// The associated [`ClusterPath`] to a process. - pub cluster_path: ClusterPath, - /// The process manager managing this process. - pub manager: Option>>, - /// The [`ChildType`] of this process. - pub current_child: Arc>, - /// When this process was last updated in [`Utc`]. - pub last_updated: DateTime, + /// The uuid of a process. + pub uuid: Uuid, + /// The associated [`ClusterPath`] to a process. + pub cluster_path: ClusterPath, + /// The process manager managing this process. + pub manager: Option>>, + /// The [`ChildType`] of this process. + pub current_child: Arc>, + /// When this process was last updated in [`Utc`]. + pub last_updated: DateTime, } impl Processor { - /// Get a new [`Processor`] instance. - pub fn new() -> Self { - Processor(HashMap::new()) - } + /// Get a new [`Processor`] instance. + pub fn new() -> Self { + Processor(HashMap::new()) + } - /// Returns a reference to the value corresponding to the key. - pub fn get(&self, uuid: &Uuid) -> Option>> { - self.0.get(uuid).cloned() - } + /// Returns a reference to the value corresponding to the key. + pub fn get(&self, uuid: &Uuid) -> Option>> { + self.0.get(uuid).cloned() + } - /// An collection visiting all keys in arbitrary order. - pub fn keys(&self) -> Vec { - self.0.keys().cloned().collect() - } + /// An collection visiting all keys in arbitrary order. + pub fn keys(&self) -> Vec { + self.0.keys().cloned().collect() + } - /// Get all running processes. - pub async fn running(&self) -> crate::Result> { - let mut keys = Vec::new(); - for key in self.keys() { - if let Some(process) = self.get(&key) { - let process = process.clone(); - let process = process.write().await; - if process.current_child.write().await.try_wait().await?.is_none() { - keys.push(key); - } - } - } - Ok(keys) - } + /// Get all running processes. + pub async fn running(&self) -> crate::Result> { + let mut keys = Vec::new(); + for key in self.keys() { + if let Some(process) = self.get(&key) { + let process = process.clone(); + let process = process.write().await; + if process + .current_child + .write() + .await + .try_wait() + .await? + .is_none() + { + keys.push(key); + } + } + } + Ok(keys) + } - /// Get all running cluster processes. - pub async fn running_cluster(&self, cluster_path: ClusterPath) -> crate::Result> { - let running = self.running().await?; - let mut keys = Vec::new(); - for key in running { - if let Some(process) = self.get(&key) { - let process = process.clone(); - let process = process.read().await; - if process.cluster_path == cluster_path { - keys.push(key); - } - } - } - Ok(keys) - } + /// Get all running cluster processes. + pub async fn running_cluster(&self, cluster_path: ClusterPath) -> crate::Result> { + let running = self.running().await?; + let mut keys = Vec::new(); + for key in running { + if let Some(process) = self.get(&key) { + let process = process.clone(); + let process = process.read().await; + if process.cluster_path == cluster_path { + keys.push(key); + } + } + } + Ok(keys) + } - /// get all running cluster paths. - pub async fn running_cluster_paths(&self) -> crate::Result> { - let mut clusters = Vec::new(); - for key in self.keys() { - if let Some(process) = self.get(&key) { - let process = process.clone(); - let process = process.write().await; - if process.current_child.write().await.try_wait().await?.is_none() { - clusters.push(process.cluster_path.clone()); - } - } - } - Ok(clusters) - } + /// get all running cluster paths. + pub async fn running_cluster_paths(&self) -> crate::Result> { + let mut clusters = Vec::new(); + for key in self.keys() { + if let Some(process) = self.get(&key) { + let process = process.clone(); + let process = process.write().await; + if process + .current_child + .write() + .await + .try_wait() + .await? + .is_none() + { + clusters.push(process.cluster_path.clone()); + } + } + } + Ok(clusters) + } - /// get all running clusters. - pub async fn running_clusters(&self) -> crate::Result> { - let mut clusters = Vec::new(); - for key in self.keys() { - if let Some(process) = self.get(&key) { - let process = process.clone(); - let process = process.write().await; - if process.current_child.write().await.try_wait().await?.is_none() { - if let Some(cluster) = cluster::get(&process.cluster_path.clone(), None).await? { - clusters.push(cluster); - } - } - } - } - Ok(clusters) - } + /// get all running clusters. + pub async fn running_clusters(&self) -> crate::Result> { + let mut clusters = Vec::new(); + for key in self.keys() { + if let Some(process) = self.get(&key) { + let process = process.clone(); + let process = process.write().await; + if process + .current_child + .write() + .await + .try_wait() + .await? + .is_none() + { + if let Some(cluster) = cluster::get(&process.cluster_path.clone(), None).await? + { + clusters.push(cluster); + } + } + } + } + Ok(clusters) + } - /// get the exit status of a process by its [`Uuid`]. - pub async fn exit_status(&self, uuid: &Uuid) -> crate::Result> { - if let Some(process) = self.get(uuid) { - let process = process.write().await; - let status = process.current_child.write().await.try_wait().await?; - Ok(status) - } else { - Ok(None) - } - } + /// get the exit status of a process by its [`Uuid`]. + pub async fn exit_status(&self, uuid: &Uuid) -> crate::Result> { + if let Some(process) = self.get(uuid) { + let process = process.write().await; + let status = process.current_child.write().await.try_wait().await?; + Ok(status) + } else { + Ok(None) + } + } - /// restore processes from the cache. - pub async fn restore(&mut self) -> crate::Result<()> { - let state = crate::State::get().await?; - let processor_path = state.directories.caches_dir().await.join(PROCESSOR_JSON); + /// restore processes from the cache. + pub async fn restore(&mut self) -> crate::Result<()> { + let state = crate::State::get().await?; + let processor_path = state.directories.caches_dir().await.join(PROCESSOR_JSON); - let mut processor_caches = if let Ok(processes_json) = - read_json::>( - &processor_path, - &state.io_semaphore, - ) - .await - { - let zeros = HashMap::::new(); - crate::utils::http::write(&processor_path, &serde_json::to_vec(&zeros)?, &state.io_semaphore).await?; - processes_json - } else { - HashMap::new() - }; + let mut processor_caches = if let Ok(processes_json) = + read_json::>(&processor_path, &state.io_semaphore) + .await + { + let zeros = HashMap::::new(); + crate::utils::http::write( + &processor_path, + &serde_json::to_vec(&zeros)?, + &state.io_semaphore, + ) + .await?; + processes_json + } else { + HashMap::new() + }; - for (_, cache) in processor_caches.drain() { - let uuid = cache.uuid; - match self.insert_restored(cache).await { - Ok(process) => { - self.0.insert(uuid, process); - } - Err(e) => tracing::warn!("failed to restore process {}: {}", uuid, e), - } - } + for (_, cache) in processor_caches.drain() { + let uuid = cache.uuid; + match self.insert_restored(cache).await { + Ok(process) => { + self.0.insert(uuid, process); + } + Err(e) => tracing::warn!("failed to restore process {}: {}", uuid, e), + } + } - Ok(()) - } + Ok(()) + } + /// insert a process with a command and [`ClusterPath`]. + #[tracing::instrument(skip(self, uuid, command, post, censors))] + #[tracing::instrument(level = "trace", skip(self))] + #[onelauncher_debug::debugger] + pub async fn insert_process( + &mut self, + uuid: Uuid, + cluster_path: ClusterPath, + mut command: Command, + post: Option, + censors: HashMap, + ) -> crate::Result>> { + let proc = command.spawn().map_err(IOError::from)?; + let process = ChildType::ChildProcess(proc); + let pid = process + .id() + .ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?; + process + .cache(uuid, cluster_path.clone(), post.clone()) + .await?; - /// insert a process with a command and [`ClusterPath`]. - #[tracing::instrument(skip(self, uuid, command, post, censors))] - #[tracing::instrument(level = "trace", skip(self))] - #[onelauncher_debug::debugger] - pub async fn insert_process( - &mut self, - uuid: Uuid, - cluster_path: ClusterPath, - mut command: Command, - post: Option, - censors: HashMap - ) -> crate::Result>> { - let proc = command.spawn().map_err(IOError::from)?; - let process = ChildType::ChildProcess(proc); - let pid = process.id().ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?; - process.cache(uuid, cluster_path.clone(), post.clone()).await?; + let current_child = Arc::new(RwLock::new(process)); + let manager = Some(tokio::spawn(Self::manager( + uuid, + post, + pid, + current_child.clone(), + cluster_path.clone(), + ))); - let current_child = Arc::new(RwLock::new(process)); - let manager = Some(tokio::spawn(Self::manager( - uuid, - post, - pid, - current_child.clone(), - cluster_path.clone(), - ))); + send_process(uuid, pid, ProcessPayloadType::Started, "started process").await?; + let last_updated = Utc::now(); + let child = ProcessorChild { + uuid, + cluster_path, + current_child, + manager, + last_updated, + }; + let child = Arc::new(RwLock::new(child)); + self.0.insert(uuid, child.clone()); + Ok(child) + } - send_process(uuid, pid, ProcessPayloadType::Started, "started process").await?; - let last_updated = Utc::now(); - let child = ProcessorChild { uuid, cluster_path, current_child, manager, last_updated }; - let child = Arc::new(RwLock::new(child)); - self.0.insert(uuid, child.clone()); - Ok(child) - } + /// Insert a cached process with a [`ProcessorCache`]. + #[tracing::instrument(skip(self, cache))] + #[tracing::instrument(level = "trace", skip(self))] + #[onelauncher_debug::debugger] + pub async fn insert_restored( + &mut self, + cache: ProcessorCache, + ) -> crate::Result>> { + let _state = crate::State::get().await?; - /// Insert a cached process with a [`ProcessorCache`]. - #[tracing::instrument(skip(self, cache))] - #[tracing::instrument(level = "trace", skip(self))] - #[onelauncher_debug::debugger] - pub async fn insert_restored( - &mut self, - cache: ProcessorCache, - ) -> crate::Result>> { - let _state = crate::State::get().await?; + // ensure located stray process matches with our pid recorded process + { + let mut system = sysinfo::System::new(); + system.refresh_processes(); + let process = system + .process(sysinfo::Pid::from_u32(cache.pid)) + .ok_or_else(|| anyhow::anyhow!("could not find pid {}", cache.pid))?; - // ensure located stray process matches with our pid recorded process - { - let mut system = sysinfo::System::new(); - system.refresh_processes(); - let process = system - .process(sysinfo::Pid::from_u32(cache.pid)) - .ok_or_else(|| anyhow::anyhow!("could not find pid {}", cache.pid))?; + if cache.start_time != process.start_time() { + return Err(anyhow::anyhow!( + "restored process {} has a mismatched start time {}", + cache.pid, + process.start_time() + ) + .into()); + } + if cache.name != process.name() { + return Err(anyhow::anyhow!( + "restored process {} has a mismatched name {}", + cache.pid, + process.name() + ) + .into()); + } - if cache.start_time != process.start_time() { - return Err(anyhow::anyhow!("restored process {} has a mismatched start time {}", cache.pid, process.start_time()).into()); - } - if cache.name != process.name() { - return Err(anyhow::anyhow!("restored process {} has a mismatched name {}", cache.pid, process.name()).into()); - } + if let Some(path) = process.exe() { + if cache.exe != path.to_string_lossy() { + return Err(anyhow::anyhow!( + "restored process {} has a mismatched exe {}", + cache.pid, + path.to_string_lossy() + ) + .into()); + } + } else { + return Err( + anyhow::anyhow!("restored process {} has no exe path", cache.pid).into(), + ); + } + } - if let Some(path) = process.exe() { - if cache.exe != path.to_string_lossy() { - return Err(anyhow::anyhow!("restored process {} has a mismatched exe {}", cache.pid, path.to_string_lossy()).into()); - } - } else { - return Err(anyhow::anyhow!("restored process {} has no exe path", cache.pid).into()); - } - } + let process = ChildType::RescuedChild(cache.pid); + let pid = process + .id() + .ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?; + process + .cache(cache.uuid, cache.cluster_path.clone(), cache.post.clone()) + .await?; + let current_child = Arc::new(RwLock::new(process)); + let manager = Some(tokio::spawn(Self::manager( + cache.uuid, + cache.post, + pid, + current_child.clone(), + cache.cluster_path.clone(), + ))); - let process = ChildType::RescuedChild(cache.pid); - let pid = process.id().ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?; - process.cache(cache.uuid, cache.cluster_path.clone(), cache.post.clone()).await?; - let current_child = Arc::new(RwLock::new(process)); - let manager = Some(tokio::spawn(Self::manager( - cache.uuid, - cache.post, - pid, - current_child.clone(), - cache.cluster_path.clone(), - ))); + send_process( + cache.uuid, + pid, + ProcessPayloadType::Started, + "started process", + ) + .await?; - send_process(cache.uuid, pid, ProcessPayloadType::Started, "started process").await?; + let last_updated = Utc::now(); + let child = ProcessorChild { + uuid: cache.uuid, + cluster_path: cache.cluster_path, + current_child, + manager, + last_updated, + }; - let last_updated = Utc::now(); - let child = ProcessorChild { - uuid: cache.uuid, - cluster_path: cache.cluster_path, - current_child, - manager, - last_updated, - }; + let child = Arc::new(RwLock::new(child)); + self.0.insert(cache.uuid, child.clone()); + Ok(child) + } - let child = Arc::new(RwLock::new(child)); - self.0.insert(cache.uuid, child.clone()); - Ok(child) - } + /// Get a process manager and runner awaiting for the exit status. + #[tracing::instrument(skip(current_child))] + #[onelauncher_debug::debugger] + async fn manager( + uuid: Uuid, + post: Option, + mut current_pid: u32, + current_child: Arc>, + cluster_path: ClusterPath, + ) -> crate::Result { + let current_child = current_child.clone(); + let mut exit_status; + let mut last_updated = Utc::now(); - /// Get a process manager and runner awaiting for the exit status. - #[tracing::instrument(skip(current_child))] - #[onelauncher_debug::debugger] - async fn manager( - uuid: Uuid, - post: Option, - mut current_pid: u32, - current_child: Arc>, - cluster_path: ClusterPath, - ) -> crate::Result { - let current_child = current_child.clone(); - let mut exit_status; - let mut last_updated = Utc::now(); + // core main process loop, managed by tokio + loop { + if let Some(stat) = current_child.write().await.try_wait().await? { + exit_status = stat; + break; + } - // core main process loop, managed by tokio - loop { - if let Some(stat) = current_child.write().await.try_wait().await? { - exit_status = stat; - break; - } + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + let update = Utc::now().signed_duration_since(last_updated).num_seconds(); + if update >= 60 { + if let Err(err) = cluster::edit(&cluster_path, |cluster| { + cluster.meta.recently_played += update as u64; + async { Ok(()) } + }) + .await + { + tracing::warn!( + "failed to update playtime for cluster {}: {}", + &cluster_path, + err + ); + } + last_updated = Utc::now(); + } + } - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - let update = Utc::now().signed_duration_since(last_updated).num_seconds(); - if update >= 60 { - if let Err(err) = cluster::edit(&cluster_path, |cluster| { - cluster.meta.recently_played += update as u64; - async { Ok(()) } - }).await { - tracing::warn!("failed to update playtime for cluster {}: {}", &cluster_path, err); - } - last_updated = Utc::now(); - } - } + let update = Utc::now().signed_duration_since(last_updated).num_seconds(); + if let Err(err) = cluster::edit(&cluster_path, |cluster| { + cluster.meta.recently_played += update as u64; + async { Ok(()) } + }) + .await + { + tracing::warn!( + "failed to update playtime for cluster {}: {}", + &cluster_path, + err + ); + } - let update = Utc::now().signed_duration_since(last_updated).num_seconds(); - if let Err(err) = cluster::edit(&cluster_path, |cluster| { - cluster.meta.recently_played += update as u64; - async { Ok(()) } - }).await { - tracing::warn!("failed to update playtime for cluster {}: {}", &cluster_path, err); - } + let cluster_path_in = cluster_path.clone(); + tokio::spawn(async move { + if let Err(err) = cluster::update_playtime(&cluster_path_in).await { + tracing::warn!( + "failed to update playtime for cluster {}: {}", + &cluster_path_in, + err + ); + } + }); - let cluster_path_in = cluster_path.clone(); - tokio::spawn(async move { - if let Err(err) = cluster::update_playtime(&cluster_path_in).await { - tracing::warn!("failed to update playtime for cluster {}: {}", &cluster_path_in, err); - } - }); + { + let state = crate::State::get().await?; + let _ = state.discord_rpc.clear(true).await; + } - { - let state = crate::State::get().await?; - let _ = state.discord_rpc.clear(true).await; - } + #[cfg(feature = "tauri")] + { + let window = crate::ProxyState::get_main_window().await?; + window.unminimize()?; + } - #[cfg(feature = "tauri")] - { - let window = crate::ProxyState::get_main_window().await?; - window.unminimize()?; - } + { + let current_child = current_child.write().await; + current_child.remove(uuid).await?; + } - { - let current_child = current_child.write().await; - current_child.remove(uuid).await?; - } + if !exit_status == 0 { + send_process( + uuid, + current_pid, + ProcessPayloadType::Finished, + "exited process", + ) + .await?; - if !exit_status == 0 { - send_process(uuid, current_pid, ProcessPayloadType::Finished, "exited process").await?; - - return Ok(exit_status); - } + return Ok(exit_status); + } - let post = if let Some(hook) = post { - let mut cmd = hook.split(' '); - if let Some(c) = cmd.next() { - let mut c = Command::new(c); - c.args(&cmd.collect::>()).current_dir(cluster_path.full_path().await?); - Some(c) - } else { - None - } - } else { - None - }; + let post = if let Some(hook) = post { + let mut cmd = hook.split(' '); + if let Some(c) = cmd.next() { + let mut c = Command::new(c); + c.args(&cmd.collect::>()) + .current_dir(cluster_path.full_path().await?); + Some(c) + } else { + None + } + } else { + None + }; - if let Some(mut m_c) = post { - { - let mut current_child: tokio::sync::RwLockWriteGuard<'_, ChildType> = current_child.write().await; - let new_child = m_c.spawn().map_err(IOError::from)?; - current_pid = new_child.id().ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?; - *current_child = ChildType::ChildProcess(new_child); - } + if let Some(mut m_c) = post { + { + let mut current_child: tokio::sync::RwLockWriteGuard<'_, ChildType> = + current_child.write().await; + let new_child = m_c.spawn().map_err(IOError::from)?; + current_pid = new_child + .id() + .ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?; + *current_child = ChildType::ChildProcess(new_child); + } - send_process(uuid, current_pid, ProcessPayloadType::Modified, "running post hook").await?; - - loop { - if let Some(stat) = current_child.write().await.try_wait().await? { - exit_status = stat; - break; - } + send_process( + uuid, + current_pid, + ProcessPayloadType::Modified, + "running post hook", + ) + .await?; - tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; - } - } + loop { + if let Some(stat) = current_child.write().await.try_wait().await? { + exit_status = stat; + break; + } - send_process(uuid, current_pid, ProcessPayloadType::Finished, "exited process").await?; + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + } - Ok(exit_status) - } + send_process( + uuid, + current_pid, + ProcessPayloadType::Finished, + "exited process", + ) + .await?; + + Ok(exit_status) + } } impl Default for Processor { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } diff --git a/core/src/store/settings.rs b/core/src/store/settings.rs index 514575b7..353e3c2f 100644 --- a/core/src/store/settings.rs +++ b/core/src/store/settings.rs @@ -1,9 +1,9 @@ //! Handles user-inputted settings and global values -use std::path::{Path, PathBuf}; -use serde::{Deserialize, Serialize}; -use crate::utils::io; use super::{Directories, JavaVersions}; +use crate::utils::io; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; /// The current [`Settings`] format version, changed for breaking changes. /// If updated, a config file migration logic **NEEDS** to be implemented. @@ -12,140 +12,146 @@ const CURRENT_FORMAT_VERSION: u32 = 1; /// A global settings state for the launcher. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Settings { - /// A OneLauncher [`Theme`] managed by the core GUI. - pub theme: Theme, - /// A global fullscreen Minecraft state. - #[serde(default)] - pub force_fullscreen: 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_jvm_args: Vec, - /// 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)] + /// A OneLauncher [`Theme`] managed by the core GUI. + pub theme: Theme, + /// A global fullscreen Minecraft state. + #[serde(default)] + pub force_fullscreen: 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, + /// 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, + /// 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, + /// Whether or not to minimize the launcher upon a game launch. + #[serde(default)] + pub hide_on_launch: bool, + /// Enable/disable advanced rendering and window decorations. + #[serde(default)] + pub rendering: bool, } impl Settings { - /// Initializes the global settings state. - #[tracing::instrument] - pub async fn initialize(file: &Path) -> crate::Result { - let mut recovered_corruption = false; + /// Initializes the global settings state. + #[tracing::instrument] + pub async fn initialize(file: &Path) -> crate::Result { + 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::(&it) - .map_err(crate::Error::from) - }); + 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::(&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; - } + 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 - }; + read_settings.ok() + } else { + None + }; - if let Some(settings) = settings { - Ok(settings) - } else { - let settings = Self { - theme: Theme::Dark, - force_fullscreen: false, - resolution: Resolution::default(), - java_versions: JavaVersions::new(), - memory: Memory::default(), - init_hooks: InitHooks::default(), - custom_env_args: Vec::new(), - custom_jvm_args: Vec::new(), - max_async_fetches: 10, - max_async_io_operations: 10, - version: CURRENT_FORMAT_VERSION, - disable_analytics: false, - disable_discord: false, - debug_mode: false, - config_dir: Directories::init_settings_dir(), - }; + if let Some(settings) = settings { + Ok(settings) + } else { + let settings = Self { + theme: Theme::Dark, + force_fullscreen: 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_FORMAT_VERSION, + disable_analytics: false, + disable_discord: false, + debug_mode: false, + config_dir: Directories::init_settings_dir(), + hide_on_launch: false, + rendering: true, + }; - if recovered_corruption { - settings.sync(file).await?; - } + if recovered_corruption { + settings.sync(file).await?; + } - Ok(settings) - } - } + 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(()) - } + /// 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(()) + } } /// A OneLauncher theme managed by the GUI. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Theme { - /// A default Dark theme. - Dark, - /// A default Light theme. - Light, - /// OLED Dark Theme. - Contrast, - /// Cute and colorful theme. - Cat, + /// A default Dark theme. + Dark, + /// A default Light theme. + Light, + /// OLED Dark Theme. + Contrast, + /// Cute and colorful theme. + Cat, } /// Global memory settings across all clusters. #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct Memory { - /// Maximum amount of Java memory available globally. - pub maximum: u32, - /// Minimum amount of Java memory available globally. - pub minimum: u32, + /// 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() -> Self { + Self { + maximum: 2048, + minimum: 256, + } + } } /// Global Minecraft resolution. @@ -153,22 +159,22 @@ impl Default for Memory { pub struct Resolution(pub u16, pub u16); impl Default for Resolution { - fn default() -> Self { - Self(854, 480) - } + fn default() -> Self { + Self(854, 480) + } } /// Global initialization hooks for all Minecraft clusters. #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(default)] pub struct InitHooks { - /// Pre-launch hook. - #[serde(skip_serializing_if = "Option::is_none")] - pub pre: Option, - /// Wrapper hook for the runtime of the game instance. - #[serde(skip_serializing_if = "Option::is_none")] - pub wrapper: Option, - /// Post-launch hook. - #[serde(skip_serializing_if = "Option::is_none")] - pub post: Option, + /// Pre-launch hook. + #[serde(skip_serializing_if = "Option::is_none")] + pub pre: Option, + /// Wrapper hook for the runtime of the game instance. + #[serde(skip_serializing_if = "Option::is_none")] + pub wrapper: Option, + /// Post-launch hook. + #[serde(skip_serializing_if = "Option::is_none")] + pub post: Option, } diff --git a/core/src/utils/http.rs b/core/src/utils/http.rs index 3acd58e0..5dd6bd55 100644 --- a/core/src/utils/http.rs +++ b/core/src/utils/http.rs @@ -1,21 +1,21 @@ //! **HTTP Utilities** -//! +//! //! Async extensions and wrappers around [`reqwest`] functions. use std::ffi::OsStr; -use std::path::PathBuf; -use std::{path::Path, time::Duration}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use bytes::Bytes; use reqwest::Method; use serde::de::DeserializeOwned; -use tokio::sync::{RwLock, Semaphore}; use tokio::fs::File; use tokio::io::AsyncWriteExt; -use bytes::Bytes; +use tokio::sync::{RwLock, Semaphore}; use crate::proxy::send::send_ingress; -use crate::utils::io; use crate::proxy::IngressId; +use crate::utils::io; use super::io::IOError; @@ -53,15 +53,7 @@ pub async fn fetch( sha1: Option<&str>, semaphore: &FetchSemaphore, ) -> crate::Result { - fetch_advanced( - Method::GET, - url, - sha1, - None, - None, - None, - semaphore, - ).await + fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore).await } /// Basic JSON fetch interface. @@ -76,15 +68,7 @@ pub async fn fetch_json( where T: DeserializeOwned, { - let result = fetch_advanced( - method, - url, - sha1, - json_body, - None, - None, - semaphore, - ).await?; + let result = fetch_advanced(method, url, sha1, json_body, None, None, semaphore).await?; let value = serde_json::from_slice(&result)?; Ok(value) @@ -120,48 +104,61 @@ pub async fn fetch_advanced( match result { Ok(x) => { - let bytes = if let Some((feed, total)) = &ingress { - let length = x.content_length(); - if let Some(total_size) = length { - use futures::StreamExt; - let mut stream = x.bytes_stream(); - let mut bytes = Vec::new(); - while let Some(item) = stream.next().await { - let chunk = item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?; - bytes.append(&mut chunk.to_vec()); - send_ingress(feed, (chunk.len() as f64 / total_size as f64) * total, None).await?; - } - - Ok(bytes::Bytes::from(bytes)) - } else { - x.bytes().await + let bytes = if let Some((feed, total)) = &ingress { + let length = x.content_length(); + if let Some(total_size) = length { + use futures::StreamExt; + let mut stream = x.bytes_stream(); + let mut bytes = Vec::new(); + while let Some(item) = stream.next().await { + let chunk = + item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?; + bytes.append(&mut chunk.to_vec()); + send_ingress( + feed, + (chunk.len() as f64 / total_size as f64) * total, + None, + ) + .await?; } + + Ok(bytes::Bytes::from(bytes)) } else { x.bytes().await - }; + } + } else { + x.bytes().await + }; - if let Ok(bytes) = bytes { - if let Some(sha1) = sha1 { - let hash = check_sha1(bytes.clone()).await?; - if &*hash != sha1 { - if attempt <= 3 { - continue; - } else { - return Err(anyhow::anyhow!("hash {0} does not match {1}", sha1.to_string(), hash).into()); - } + if let Ok(bytes) = bytes { + if let Some(sha1) = sha1 { + let hash = check_sha1(bytes.clone()).await?; + if &*hash != sha1 { + if attempt <= 3 { + continue; + } else { + return Err(anyhow::anyhow!( + "hash {0} does not match {1}", + sha1.to_string(), + hash + ) + .into()); } } - - tracing::trace!("finished downloading {url}"); - return Ok(bytes); - } else if attempt <= 3 { - continue; - } else if let Err(err) = bytes { - return Err(err.into()); } + + tracing::trace!("finished downloading {url}"); + return Ok(bytes); + } else if attempt <= 3 { + continue; + } else if let Err(err) = bytes { + return Err(err.into()); } - Err(_) if attempt <= 3 => continue, - Err(err) => { return Err(err.into()); } + } + Err(_) if attempt <= 3 => continue, + Err(err) => { + return Err(err.into()); + } } } @@ -225,10 +222,7 @@ where } /// Read JSON from a specified [`Path`]. -pub async fn read_json( - path: &Path, - semaphore: &IoSemaphore, -) -> crate::Result +pub async fn read_json(path: &Path, semaphore: &IoSemaphore) -> crate::Result where T: DeserializeOwned, { @@ -243,11 +237,7 @@ where /// Write to a file at a specified [`Path`]. #[tracing::instrument(skip(bytes, semaphore))] -pub async fn write<'a>( - path: &Path, - bytes: &[u8], - semaphore: &IoSemaphore, -) -> crate::Result<()> { +pub async fn write<'a>(path: &Path, bytes: &[u8], semaphore: &IoSemaphore) -> crate::Result<()> { let io_semaphore = semaphore.0.read().await; let _permit = io_semaphore.acquire().await?; @@ -255,8 +245,12 @@ pub async fn write<'a>( io::create_dir_all(parent).await?; } - let mut file = File::create(path).await.map_err(|e| IOError::with_path(e, path))?; - file.write_all(bytes).await.map_err(|e| IOError::with_path(e, path))?; + let mut file = File::create(path) + .await + .map_err(|e| IOError::with_path(e, path))?; + file.write_all(bytes) + .await + .map_err(|e| IOError::with_path(e, path))?; tracing::trace!("done writing to file {}", path.display()); Ok(()) } @@ -278,16 +272,29 @@ pub async fn copy( } io::copy(src, dest).await?; - tracing::trace!("done copying file from {} to {}", src.display(), dest.display()); + tracing::trace!( + "done copying file from {} to {}", + src.display(), + dest.display() + ); Ok(()) } #[tracing::instrument(skip(bytes, io_semaphore))] -pub async fn write_icon(icon_path: &str, cache_path: &Path, bytes: Bytes, io_semaphore: &IoSemaphore) -> crate::Result { +pub async fn write_icon( + icon_path: &str, + cache_path: &Path, + bytes: Bytes, + io_semaphore: &IoSemaphore, +) -> crate::Result { let ext = Path::new(&icon_path).extension().and_then(OsStr::to_str); let hash = check_sha1(bytes.clone()).await?; - let path = cache_path.join("icons").join(if let Some(e) = ext { format!("{hash}.{e}") } else { hash }); - + let path = cache_path.join("icons").join(if let Some(e) = ext { + format!("{hash}.{e}") + } else { + hash + }); + write(&path, &bytes, io_semaphore).await?; let path = io::canonicalize(path)?; @@ -296,9 +303,8 @@ pub async fn write_icon(icon_path: &str, cache_path: &Path, bytes: Bytes, io_sem /// Get the Sha1 hash of [`bytes::Bytes`] array. pub async fn check_sha1(bytes: Bytes) -> crate::Result { - let hash = tokio::task::spawn_blocking(move || { - sha1_smol::Sha1::from(bytes).hexdigest() - }).await?; + let hash = + tokio::task::spawn_blocking(move || sha1_smol::Sha1::from(bytes).hexdigest()).await?; Ok(hash) } diff --git a/core/src/utils/io.rs b/core/src/utils/io.rs index d28b370d..bc573b08 100644 --- a/core/src/utils/io.rs +++ b/core/src/utils/io.rs @@ -1,12 +1,14 @@ //! **IO Utilities** -//! +//! //! Wrapper around [`tokio::io`] for our error system. -use std::{fs::File, io::Write, path::PathBuf}; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; use anyhow::anyhow; -use tempfile::NamedTempFile; use tauri::async_runtime::spawn_blocking; +use tempfile::NamedTempFile; /// A wrapper around generic and unhelpful [`std::io::Error`] messages. #[derive(Debug, thiserror::Error)] @@ -32,21 +34,15 @@ impl IOError { Self::ZipError(source) } - pub fn with_path( - source: std::io::Error, - path: impl AsRef, - ) -> Self { + pub fn with_path(source: std::io::Error, path: impl AsRef) -> Self { let path = path.as_ref().to_string_lossy().to_string(); Self::IOErrorWrapper { source, path } } } - /// An OS specific wrapper of [`std::fs::canonicalize`], but on Windows it outputs the most compatible form of a path instead of UNC. -pub fn canonicalize( - path: impl AsRef, -) -> Result { +pub fn canonicalize(path: impl AsRef) -> Result { let path = path.as_ref(); dunce::canonicalize(path).map_err(|e| IOError::IOErrorWrapper { source: e, @@ -55,9 +51,7 @@ pub fn canonicalize( } /// Returns a stream over the entries within a directory. -pub async fn read_dir( - path: impl AsRef, -) -> Result { +pub async fn read_dir(path: impl AsRef) -> Result { let path = path.as_ref(); tokio::fs::read_dir(path) .await @@ -68,56 +62,65 @@ pub async fn read_dir( } /// Recursively creates a directory and all of its parent components if they are missing. -pub async fn create_dir_all( - path: impl AsRef, -) -> Result<(), IOError> { +pub async fn create_dir_all(path: impl AsRef) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::create_dir_all(path) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: path.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: path.to_string_lossy().to_string(), + }) } /// Removes a directory at this path, after removing all its contents. Use carefully! -pub async fn remove_dir_all( - path: impl AsRef, -) -> Result<(), IOError> { +pub async fn remove_dir_all(path: impl AsRef) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::remove_dir_all(path) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: path.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: path.to_string_lossy().to_string(), + }) } /// Creates a future which will open a file for reading and read the entire contents into a string and return said string. -pub async fn read_to_string( - path: impl AsRef, -) -> Result { +pub async fn read_to_string(path: impl AsRef) -> Result { let path = path.as_ref(); tokio::fs::read_to_string(path) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: path.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: path.to_string_lossy().to_string(), + }) } /// Reads the entire contents of a file into a bytes vector. -pub async fn read( - path: impl AsRef, -) -> Result, IOError> { +pub async fn read(path: impl AsRef) -> Result, IOError> { let path = path.as_ref(); tokio::fs::read(path) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: path.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: path.to_string_lossy().to_string(), + }) } /// Asynchrously write to a tempfile that is then transferred to an official [`AsRef`]. pub async fn write( path: impl AsRef, - data: impl AsRef<[u8]> + data: impl AsRef<[u8]>, ) -> Result<(), IOError> { let path = path.as_ref().to_owned(); let data = data.as_ref().to_owned(); spawn_blocking(move || { let cloned_path = path.clone(); - sync_write(data, path).map_err(|e| IOError::IOErrorWrapper { source: e, path: cloned_path.to_string_lossy().to_string() }) - }).await.map_err(|_| {std::io::Error::new(std::io::ErrorKind::Other, "tokio task failed")})??; + sync_write(data, path).map_err(|e| IOError::IOErrorWrapper { + source: e, + path: cloned_path.to_string_lossy().to_string(), + }) + }) + .await + .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "tokio task failed"))??; Ok(()) } @@ -150,7 +153,10 @@ pub async fn rename( let to = to.as_ref(); tokio::fs::rename(from, to) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: from.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: from.to_string_lossy().to_string(), + }) } /// Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. This function will overwrite the contents of to. @@ -162,17 +168,21 @@ pub async fn copy( let to = to.as_ref(); tokio::fs::copy(from, to) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: from.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: from.to_string_lossy().to_string(), + }) } /// Removes a file from the filesystem. -pub async fn remove_file( - path: impl AsRef, -) -> Result<(), IOError> { +pub async fn remove_file(path: impl AsRef) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::remove_file(path) .await - .map_err(|e| IOError::IOErrorWrapper { source: e, path: path.to_string_lossy().to_string() }) + .map_err(|e| IOError::IOErrorWrapper { + source: e, + path: path.to_string_lossy().to_string(), + }) } /// Extract an archive based on its file extension, supporting ZIP files and GZ files. @@ -202,8 +212,6 @@ pub fn extract_tar_gz(archive: &PathBuf, dest: &PathBuf) -> Result<(), IOError> let file = File::open(archive).map_err(|err| IOError::from(err))?; let tar_gz = flate2::read::GzDecoder::new(file); let mut archive = tar::Archive::new(tar_gz); - archive - .unpack(dest) - .map_err(|err| IOError::from(err))?; + archive.unpack(dest).map_err(|err| IOError::from(err))?; Ok(()) } diff --git a/core/src/utils/java.rs b/core/src/utils/java.rs index 5e5eef70..269304d9 100644 --- a/core/src/utils/java.rs +++ b/core/src/utils/java.rs @@ -1,17 +1,20 @@ //! **Java Utilities** -//! +//! //! Async utilities for managing and downloading Java versions. -use std::{collections::HashSet, env, path::{Path, PathBuf}, process::Command}; -use futures::prelude::*; -use serde::{Serialize, Deserialize}; -use tokio::task::JoinError; use crate::State; +use futures::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Command; +use tokio::task::JoinError; #[cfg(target_os = "windows")] use winreg::{ - enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY}, - RegKey + enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WOW64_32KEY, KEY_WOW64_64KEY}, + RegKey, }; use super::io; @@ -22,195 +25,198 @@ use super::io; /// paths and java_check from https://github.com/modrinth/theseus/blob/master/theseus/src/util/jre.rs under MIT #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)] pub struct JavaVersion { - pub version: String, - pub arch: String, - pub path: String, + pub version: String, + pub arch: String, + pub path: String, } /// JVM Entrypoint (WINDOWS ONLY) #[cfg(target_os = "windows")] #[tracing::instrument] pub async fn locate_java() -> Result, JavaError> { - let mut paths = HashSet::new(); - paths.extend(locate_java_path().await); - paths.extend(locate_installed_java().await?); - if let Ok(java_home) = env::var("JAVA_HOME") { - paths.insert(PathBuf::from(java_home)); - } + let mut paths = HashSet::new(); + paths.extend(locate_java_path().await); + paths.extend(locate_installed_java().await?); + if let Ok(java_home) = env::var("JAVA_HOME") { + paths.insert(PathBuf::from(java_home)); + } - // i hate windows.com - let common_java_paths = [ - r"C:/Program Files/Java", - r"C:\Program Files\Eclipse Adoptium", - r"C:/Program Files (x86)/Java", - r"C:\Program Files (x86)/Eclipse Adoptium", - ]; + // i hate windows.com + let common_java_paths = [ + r"C:/Program Files/Java", + r"C:\Program Files\Eclipse Adoptium", + r"C:/Program Files (x86)/Java", + r"C:\Program Files (x86)/Eclipse Adoptium", + ]; - for path in common_java_paths { - let Ok(java_subs) = std::fs::read_dir(path) else { continue; }; - for java_sub in java_subs.flatten() { - let full_path = java_sub.path(); - paths.insert(full_path.join("bin")); - } - } + for path in common_java_paths { + let Ok(java_subs) = std::fs::read_dir(path) else { + continue; + }; + for java_sub in java_subs.flatten() { + let full_path = java_sub.path(); + paths.insert(full_path.join("bin")); + } + } - let common_key_paths = [ - r"SOFTWARE\\JavaSoft\\Java Runtime Environment", - r"SOFTWARE\\JavaSoft\\Java Development Kit", - r"SOFTWARE\\JavaSoft\\JRE", - r"SOFTWARE\\JavaSoft\\JDK", - r"SOFTWARE\\AdoptOpenJDK\\JRE", - r"SOFTWARE\\AdoptOpenJDK\\JDK", - r"SOFTWARE\\Eclipse Foundation\\JDK", - r"SOFTWARE\\Eclipse Foundation\\JRE", - r"SOFTWARE\\Eclipse Adoptium\\JRE", - r"SOFTWARE\\Eclipse Adoptium\\JDK", - r"SOFTWARE\\Microsoft\\JDK", - r"SOFTWARE\\Azul Systems\\Zulu", - r"SOFTWARE\\BellSoft\\Liberica", - ]; + let common_key_paths = [ + r"SOFTWARE\\JavaSoft\\Java Runtime Environment", + r"SOFTWARE\\JavaSoft\\Java Development Kit", + r"SOFTWARE\\JavaSoft\\JRE", + r"SOFTWARE\\JavaSoft\\JDK", + r"SOFTWARE\\AdoptOpenJDK\\JRE", + r"SOFTWARE\\AdoptOpenJDK\\JDK", + r"SOFTWARE\\Eclipse Foundation\\JDK", + r"SOFTWARE\\Eclipse Foundation\\JRE", + r"SOFTWARE\\Eclipse Adoptium\\JRE", + r"SOFTWARE\\Eclipse Adoptium\\JDK", + r"SOFTWARE\\Microsoft\\JDK", + r"SOFTWARE\\Azul Systems\\Zulu", + r"SOFTWARE\\BellSoft\\Liberica", + ]; - for key in key_paths { - if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE) - .open_subkey_with_flags(key, KEY_READ | KEY_WOW64_32KEY) - { - paths.extend(get_java_paths_from_windows(java_key)); - } - if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE) - .open_subkey_with_flags(key, KEY_READ | KEY_WOW64_64KEY) - { - paths.extend(get_java_paths_from_windows(java_key)); - } - } + for key in key_paths { + if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey_with_flags(key, KEY_READ | KEY_WOW64_32KEY) + { + paths.extend(get_java_paths_from_windows(java_key)); + } + if let Ok(java_key) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey_with_flags(key, KEY_READ | KEY_WOW64_64KEY) + { + paths.extend(get_java_paths_from_windows(java_key)); + } + } - let java = check_java(paths).await.into_iter().collect(); + let java = check_java(paths).await.into_iter().collect(); - Ok(java); + Ok(java); } #[cfg(target_os = "windows")] #[tracing::instrument] pub fn get_java_paths_from_windows(java_key: RegKey) -> HashSet { - let mut paths = HashSet::new(); + let mut paths = HashSet::new(); - for subkey in java_key.enum_keys().flatten() { - if let Ok(subkey) = java_key.open_subkey(subkey) { - let common_subkey_value_names = [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"]; - for subkey_value in common_subkey_value_names { - let path: Result = subkey.get_value(subkey_value); - let Ok(path) = path else { continue }; - paths.insert(PathBuf::from(path).join("bin")); - } - } - } + for subkey in java_key.enum_keys().flatten() { + if let Ok(subkey) = java_key.open_subkey(subkey) { + let common_subkey_value_names = [r"JavaHome", r"InstallationPath", r"\\hotspot\\MSI"]; + for subkey_value in common_subkey_value_names { + let path: Result = subkey.get_value(subkey_value); + let Ok(path) = path else { continue }; + paths.insert(PathBuf::from(path).join("bin")); + } + } + } - paths + paths } /// JVM Entrypoint (MacOS ONLY) #[cfg(target_os = "macos")] #[tracing::instrument] pub async fn locate_java() -> Result, JavaError> { - let mut paths = HashSet::new(); - paths.extend(locate_java_path().await); - paths.extend(locate_installed_java().await?); + let mut paths = HashSet::new(); + paths.extend(locate_java_path().await); + paths.extend(locate_installed_java().await?); - let jvm_base = PathBuf::from("/Library/Java/JavaVirtualMachines/"); - if let Ok(dir) = std::fs::read_dir(jvm_base) { - for entry in dir.flatten() { - let entry = entry.path().join("Contents/Home/bin"); - paths.insert(entry); - } - } + let jvm_base = PathBuf::from("/Library/Java/JavaVirtualMachines/"); + if let Ok(dir) = std::fs::read_dir(jvm_base) { + for entry in dir.flatten() { + let entry = entry.path().join("Contents/Home/bin"); + paths.insert(entry); + } + } - let common_java_paths = [ - r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands", - r"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java", - r"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home", - ]; + let common_java_paths = [ + r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands", + r"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java", + r"/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home", + ]; - for path in common_java_paths { - paths.insert(PathBuf::from(path)); - } + for path in common_java_paths { + paths.insert(PathBuf::from(path)); + } - let java = check_java(paths).await.into_iter().collect(); + let java = check_java(paths).await.into_iter().collect(); - Ok(java) + Ok(java) } /// JVM Entrypoint (LINUX ONLY) #[cfg(target_os = "linux")] #[tracing::instrument] pub async fn locate_java() -> Result, JavaError> { - let mut paths = HashSet::new(); - paths.extend(locate_java_path().await); - paths.extend(locate_installed_java().await?); + let mut paths = HashSet::new(); + paths.extend(locate_java_path().await); + paths.extend(locate_installed_java().await?); - let common_java_paths = [ - r"/usr", - r"/usr/java", - r"/usr/lib/jvm", - r"/usr/lib64/jvm", - r"/usr/lib32/jvm", - r"/opt/jdk", - r"/opt/jdks", - r"/app/jdk", - ]; + let common_java_paths = [ + r"/usr", + r"/usr/java", + r"/usr/lib/jvm", + r"/usr/lib64/jvm", + r"/usr/lib32/jvm", + r"/opt/jdk", + r"/opt/jdks", + r"/app/jdk", + ]; - for path in common_java_paths { - let path = PathBuf::from(path); - paths.insert(PathBuf::from(&path).join("jre").join("bin")); - paths.insert(PathBuf::from(&path).join("bin")); - if let Ok(dir) = std::fs::read_dir(path) { - for entry in dir.flatten() { - let entry_path = entry.path(); - paths.insert(entry_path.join("jre").join("bin")); - paths.insert(entry_path.join("bin")); - } - } - } + for path in common_java_paths { + let path = PathBuf::from(path); + paths.insert(PathBuf::from(&path).join("jre").join("bin")); + paths.insert(PathBuf::from(&path).join("bin")); + if let Ok(dir) = std::fs::read_dir(path) { + for entry in dir.flatten() { + let entry_path = entry.path(); + paths.insert(entry_path.join("jre").join("bin")); + paths.insert(entry_path.join("bin")); + } + } + } - let java = check_java(paths).await.into_iter().collect(); + let java = check_java(paths).await.into_iter().collect(); - Ok(java) + Ok(java) } /// Locate JRE's auto-installed by the launcher instance. #[tracing::instrument] #[onelauncher_debug::debugger] async fn locate_installed_java() -> Result, JavaError> { - Box::pin(async move { - let state = State::get().await.map_err(|_| JavaError::MutexError)?; - let mut paths = HashSet::new(); - let java_base_path = state.directories.java_dir().await; + Box::pin(async move { + let state = State::get().await.map_err(|_| JavaError::MutexError)?; + let mut paths = HashSet::new(); + let java_base_path = state.directories.java_dir().await; - if java_base_path.is_dir() { - if let Ok(dir) = std::fs::read_dir(java_base_path) { - for entry in dir.flatten() { - let path = entry.path().join("bin"); - if let Ok(contents) = io::read_to_string(path.clone()).await { - let entry = entry.path().join(contents); - paths.insert(entry); - } else { - #[cfg(not(target_os = "macos"))] - { - let path = path.join(JAVA_BIN); - paths.insert(path); - } - } - } - } - } + if java_base_path.is_dir() { + if let Ok(dir) = std::fs::read_dir(java_base_path) { + for entry in dir.flatten() { + let path = entry.path().join("bin"); + if let Ok(contents) = io::read_to_string(path.clone()).await { + let entry = entry.path().join(contents); + paths.insert(entry); + } else { + #[cfg(not(target_os = "macos"))] + { + let path = path.join(JAVA_BIN); + paths.insert(path); + } + } + } + } + } - Ok(paths) - }).await + Ok(paths) + }) + .await } /// Locate all installed Java instances from the PATH env variable cross-device. #[tracing::instrument] async fn locate_java_path() -> HashSet { - let paths = env::var("PATH").map(|p| env::split_paths(&p).collect::>()); - paths.unwrap_or_else(|_| HashSet::new()) + let paths = env::var("PATH").map(|p| env::split_paths(&p).collect::>()); + paths.unwrap_or_else(|_| HashSet::new()) } #[cfg(target_os = "windows")] @@ -222,19 +228,17 @@ pub const JAVA_BIN: &str = "java"; /// Verifies that each Java instance is valid from a HashSet of PathBufs #[tracing::instrument] pub async fn check_java(paths: HashSet) -> HashSet { - let java_instances = stream::iter(paths.into_iter()) - .map(|p: PathBuf| { - tokio::task::spawn(async move { check_java_instance(&p).await }) - }) - .buffer_unordered(64) - .collect::>() - .await; + let java_instances = stream::iter(paths.into_iter()) + .map(|p: PathBuf| tokio::task::spawn(async move { check_java_instance(&p).await })) + .buffer_unordered(64) + .collect::>() + .await; - java_instances - .into_iter() - .flat_map(|p| p.ok()) - .flatten() - .collect() + java_instances + .into_iter() + .flat_map(|p| p.ok()) + .flatten() + .collect() } /// Verifies that a java instance [`Path`] is valid. @@ -242,91 +246,105 @@ pub async fn check_java(paths: HashSet) -> HashSet { #[tracing::instrument] #[onelauncher_debug::debugger] pub async fn check_java_instance(path: &Path) -> Option { - let Ok(path) = io::canonicalize(path) else { return None; }; - let java = if path.file_name()?.to_str()? != JAVA_BIN { path.join(JAVA_BIN) } else { path }; - if !java.exists() { return None; } + let Ok(path) = io::canonicalize(path) else { + return None; + }; + let java = if path.file_name()?.to_str()? != JAVA_BIN { + path.join(JAVA_BIN) + } else { + path + }; + if !java.exists() { + return None; + } - let bytes = include_bytes!("../../wrapper/java/JavaInfo.class"); - let tempdir: PathBuf = tempfile::tempdir().ok()?.into_path(); - if !tempdir.exists() { return None; } + let bytes = include_bytes!("../../wrapper/java/JavaInfo.class"); + let tempdir: PathBuf = tempfile::tempdir().ok()?.into_path(); + if !tempdir.exists() { + return None; + } - let file_path = tempdir.join("JavaInfo.class"); - io::write(&file_path, bytes).await.ok()?; + let file_path = tempdir.join("JavaInfo.class"); + io::write(&file_path, bytes).await.ok()?; - let output = Command::new(&java) - .arg("-cp") - .arg(file_path.parent().unwrap()) - .arg("JavaInfo").output().ok()?; - let stdout = String::from_utf8_lossy(&output.stdout); + let output = Command::new(&java) + .arg("-cp") + .arg(file_path.parent().unwrap()) + .arg("JavaInfo") + .output() + .ok()?; + let stdout = String::from_utf8_lossy(&output.stdout); - let mut java_version = None; - let mut java_arch = None; + let mut java_version = None; + let mut java_arch = None; - for line in stdout.lines() { - let mut parts = line.split('='); - let key = parts.next().unwrap_or_default(); - let value = parts.next().unwrap_or_default(); + for line in stdout.lines() { + let mut parts = line.split('='); + let key = parts.next().unwrap_or_default(); + let value = parts.next().unwrap_or_default(); - if key == "os.arch" { - java_arch = Some(value); - } else if key == "java.version" { - java_version = Some(value); - } - } + if key == "os.arch" { + java_arch = Some(value); + } else if key == "java.version" { + java_version = Some(value); + } + } - if let Some(arch) = java_arch { - if let Some(version) = java_version { - let path = java.to_string_lossy().to_string(); - return Some(JavaVersion { - path, - version: version.to_string(), - arch: arch.to_string(), - }); - } - } + if let Some(arch) = java_arch { + if let Some(version) = java_version { + let path = java.to_string_lossy().to_string(); + return Some(JavaVersion { + path, + version: version.to_string(), + arch: arch.to_string(), + }); + } + } - None + None } /// Get a formatted java version. pub fn get_java_version(version: &str) -> Result<(u32, u32), JavaError> { - let mut split = version.split('.'); - let major_opt = split.next(); - let mut major; - let mut minor = if let Some(minor) = split.next() { - major = major_opt.unwrap_or("1").parse::()?; - minor.parse::()? - } else { - major = 1; - major_opt.ok_or_else(|| JavaError::InvalidVersion(version.to_string()))?.parse::()? - }; + let mut split = version.split('.'); + let major_opt = split.next(); + let mut major; + let mut minor = if let Some(minor) = split.next() { + major = major_opt.unwrap_or("1").parse::()?; + minor.parse::()? + } else { + major = 1; + major_opt + .ok_or_else(|| JavaError::InvalidVersion(version.to_string()))? + .parse::()? + }; - if major > 1 { - minor = major; - major = 1; - } + if major > 1 { + minor = major; + major = 1; + } - Ok((major, minor)) + Ok((major, minor)) } /// An error wrapper for everything that can go wrong while managing Java installations. #[derive(thiserror::Error, Debug)] pub enum JavaError { - #[error("failed to fetch environment variable: {0}")] - EnvError(#[from] env::VarError), + #[error("failed to fetch environment variable: {0}")] + EnvError(#[from] env::VarError), - #[error("error running java command: {0}")] - IOError(#[from] std::io::Error), + #[error("error running java command: {0}")] + IOError(#[from] std::io::Error), - #[error("failed to parse integer: {0}")] - ParseError(#[from] std::num::ParseIntError), + #[error("failed to parse integer: {0}")] + ParseError(#[from] std::num::ParseIntError), - #[error("tokio error: {0}")] - JoinError(#[from] JoinError), + #[error("tokio error: {0}")] + JoinError(#[from] JoinError), - #[error("couldn't get a valid java version from {0}")] - InvalidVersion(String), + #[error("couldn't get a valid java version from {0}")] + InvalidVersion(String), - #[error("failed to get launcher state mutex!")] - MutexError, + #[error("failed to get launcher state mutex!")] + MutexError, } diff --git a/core/src/utils/mod.rs b/core/src/utils/mod.rs index c4ab2775..143e3b16 100644 --- a/core/src/utils/mod.rs +++ b/core/src/utils/mod.rs @@ -1,20 +1,20 @@ //! OneLauncher utility functions -pub mod io; pub mod http; -pub mod platform; +pub mod io; pub mod java; -pub mod watcher; pub mod pkg; +pub mod platform; +pub mod watcher; /// mutable reference gets epically owned by free thinking macro!!!! (not clickbait) /// im going insane insane insane insane insane insane macro_rules! ref_owned { - ($id:ident = $init:expr => $transform:block) => {{ - let mut it = $init; - { - let $id = &mut it; - $transform; - } - it - }}; + ($id:ident = $init:expr => $transform:block) => {{ + let mut it = $init; + { + let $id = &mut it; + $transform; + } + it + }}; } diff --git a/core/src/utils/pkg.rs b/core/src/utils/pkg.rs index e69de29b..8b137891 100644 --- a/core/src/utils/pkg.rs +++ b/core/src/utils/pkg.rs @@ -0,0 +1 @@ + diff --git a/core/src/utils/platform.rs b/core/src/utils/platform.rs index c765fb3e..1f79264d 100644 --- a/core/src/utils/platform.rs +++ b/core/src/utils/platform.rs @@ -1,5 +1,5 @@ //! **Platform Utilities** -//! +//! //! Async utilities for managing OS-specific [`interpulse`] operations and rules. use interpulse::api::minecraft::{Os, OsRule}; @@ -58,11 +58,7 @@ pub const ARCH_WIDTH: &str = "64"; pub const ARCH_WIDTH: &str = "32"; // Platform rule handling -pub fn os_rule( - rule: &OsRule, - java_arch: &str, - updated: bool, -) -> bool { +pub fn os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool { let mut rule_match = true; if let Some(ref arch) = rule.arch { diff --git a/core/src/utils/watcher.rs b/core/src/utils/watcher.rs index f966662e..fe06389d 100644 --- a/core/src/utils/watcher.rs +++ b/core/src/utils/watcher.rs @@ -1,74 +1,79 @@ //! **Watcher Utilities* -//! +//! //! Async utilities for watching files with [`notify`]. -use std::{path::PathBuf, time::Duration}; +use std::path::PathBuf; +use std::time::Duration; +use crate::store::{Cluster, ClusterPath, Clusters}; +use futures::channel::mpsc::channel; +use futures::{SinkExt, StreamExt}; use notify::RecommendedWatcher; use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; -use futures::{channel::mpsc::channel, SinkExt, StreamExt}; -use crate::store::{Cluster, ClusterPath, Clusters}; pub const CLUSTERS_PATH: &str = "clusters"; pub const CRASH_PATH: &str = "crash-reports"; pub async fn initialize_watcher() -> crate::Result> { - let (mut sender, mut rscv) = channel(1); - let watcher = new_debouncer( - Duration::from_secs_f32(2.0), - move |result: DebounceEventResult| { - futures::executor::block_on(async { - sender.send(result).await.unwrap(); - }) - })?; + let (mut sender, mut rscv) = channel(1); + let watcher = new_debouncer( + Duration::from_secs_f32(2.0), + move |result: DebounceEventResult| { + futures::executor::block_on(async { + sender.send(result).await.unwrap(); + }) + }, + )?; - tokio::task::spawn(async move { - let span = tracing::span!(tracing::Level::INFO, "initialize_watcher"); - tracing::info!(parent: &span, "initializing fs watcher..."); - while let Some(result) = rscv.next().await { - let _span = span.enter(); + tokio::task::spawn(async move { + let span = tracing::span!(tracing::Level::INFO, "initialize_watcher"); + tracing::info!(parent: &span, "initializing fs watcher..."); + while let Some(result) = rscv.next().await { + let _span = span.enter(); - match result { - Ok(mut events) => { - let mut paths = Vec::new(); - events.sort_by(|a, b| a.path.cmp(&b.path)); - events.iter().for_each(|a| { - let mut formatted = PathBuf::new(); - let mut components = a.path.components(); - let mut matched = false; + match result { + Ok(mut events) => { + let mut paths = Vec::new(); + events.sort_by(|a, b| a.path.cmp(&b.path)); + events.iter().for_each(|a| { + let mut formatted = PathBuf::new(); + let mut components = a.path.components(); + let mut matched = false; - for cmp in components.by_ref() { - formatted.push(cmp); - if matched { break; } - // really simple fs watcher to check in any cluster files have been modified by outside forces - if cmp.as_os_str() == "clusters" { matched = true; } - } + for cmp in components.by_ref() { + formatted.push(cmp); + if matched { + break; + } + // really simple fs watcher to check in any cluster files have been modified by outside forces + if cmp.as_os_str() == "clusters" { + matched = true; + } + } - let sub = components.next().is_none(); - let cluster_path = ClusterPath::new(PathBuf::from( - formatted.file_name().unwrap_or_default(), - )); + let sub = components.next().is_none(); + let cluster_path = ClusterPath::new(PathBuf::from( + formatted.file_name().unwrap_or_default(), + )); - if a.path - .components() - .any(|c| c.as_os_str() == CRASH_PATH) - && a.path.extension().map(|e| e == "txt").unwrap_or(false) - { - Cluster::handle_crash(cluster_path); - } else if !paths.contains(&formatted) { - if sub { - Cluster::sync_packages(cluster_path, false); - paths.push(formatted); - } else { - Clusters::sync_clusters(cluster_path); - } - } - }); - } - Err(err) => tracing::warn!("fs watching error: {err}") - } - } - }); + if a.path.components().any(|c| c.as_os_str() == CRASH_PATH) + && a.path.extension().map(|e| e == "txt").unwrap_or(false) + { + Cluster::handle_crash(cluster_path); + } else if !paths.contains(&formatted) { + if sub { + Cluster::sync_packages(cluster_path, false); + paths.push(formatted); + } else { + Clusters::sync_clusters(cluster_path); + } + } + }); + } + Err(err) => tracing::warn!("fs watching error: {err}"), + } + } + }); - Ok(watcher) + Ok(watcher) } diff --git a/desktop/src-tauri/src/api/mod.rs b/desktop/src-tauri/src/api/mod.rs index e187cd44..15ae84d9 100644 --- a/desktop/src-tauri/src/api/mod.rs +++ b/desktop/src-tauri/src/api/mod.rs @@ -1,11 +1,10 @@ -use onelauncher::{game::client_manager::ClientManager, AppState}; +use onelauncher::game::client_manager::ClientManager; +use onelauncher::AppState; use serde::{Serialize, Serializer}; -use tauri::{plugin::TauriPlugin, Manager, State}; +use tauri::plugin::TauriPlugin; +use tauri::{Manager, State}; use tokio::sync::Mutex; -pub mod auth; -pub mod game; - pub fn init() -> TauriPlugin { tauri::plugin::Builder::new("onelauncher") .setup(|app, _| { @@ -13,14 +12,7 @@ pub fn init() -> TauriPlugin { Ok(()) }) - .invoke_handler(tauri::generate_handler![ - auth::login_msa, - game::create_cluster, - game::get_clusters, - game::get_cluster, - game::launch_cluster, - refresh_client_manager, - ]) + .invoke_handler(tauri::generate_handler![]) .build() } diff --git a/package.json b/package.json index ebfd2d92..bf854a84 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "release:node": "bumpp -r" }, "devDependencies": { - "@flowr/eslint-config": "^3.0.0", + "@flowr/eslint-config": "^3.0.1", "@types/node": "^20.12.7", "bumpp": "^9.4.0", "eslint": "^8.56.0", - "eslint-plugin-solid": "^0.13.2", + "eslint-plugin-solid": "^0.14.0", "typescript": "^5.4.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40b0f7e6..ff9974f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,17 +9,20 @@ importers: .: devDependencies: '@flowr/eslint-config': - specifier: ^3.0.0 - version: 3.0.0(@unocss/eslint-plugin@0.59.4(eslint@8.57.0)(typescript@5.4.5))(@vue/compiler-sfc@3.4.25)(eslint-plugin-solid@0.13.2(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(vitest@1.5.2(@types/node@20.12.7)(sass@1.75.0)) + specifier: ^3.0.1 + version: 3.0.1(@unocss/eslint-plugin@0.59.4(eslint@8.57.0)(typescript@5.4.5))(@vue/compiler-sfc@3.4.25)(eslint-plugin-solid@0.14.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(vitest@1.5.2(@types/node@20.12.7)(sass@1.75.0)) '@types/node': specifier: ^20.12.7 version: 20.12.7 + bumpp: + specifier: ^9.4.0 + version: 9.4.0 eslint: specifier: ^8.56.0 version: 8.57.0 eslint-plugin-solid: - specifier: ^0.13.2 - version: 0.13.2(eslint@8.57.0)(typescript@5.4.5) + specifier: ^0.14.0 + version: 0.14.0(eslint@8.57.0)(typescript@5.4.5) typescript: specifier: ^5.4.5 version: 5.4.5 @@ -418,8 +421,8 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@flowr/eslint-config@3.0.0': - resolution: {integrity: sha512-0xYBbcA0JA9VjOjDBEn2rIYR1WhiCOUrojgzC0zV7+dj/LI05iQFJd0VtAJPo4MnanCswBWT4K2OPJZGU094TA==} + '@flowr/eslint-config@3.0.1': + resolution: {integrity: sha512-2Wanv3zfIt8m26F7cfWeQOhiRJbT8aCtu5iLJOmS3ENAHvIo4EEfg6XOF3TKOTLZJNhANnoUzTBzTEq3Ucgdjw==} engines: {node: '>=20'} peerDependencies: '@eslint-react/eslint-plugin': ^1.5.9 @@ -1167,6 +1170,14 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + bumpp@9.4.0: + resolution: {integrity: sha512-T+N+B5mSSvsTt7kgxGXCKQd957IxmEZwMn2nWirspGUY08u/cBNrpQDMXk43fzLegZtKtXS1v9S06vq2zf6lXQ==} + engines: {node: '>=10'} + hasBin: true + + c12@1.10.0: + resolution: {integrity: sha512-0SsG7UDhoRWcuSvKWHaXmu5uNjDCDN3nkQLRL4Q42IlFy+ze58FcCoI3uPwINXinkz7ZinbhEgyzYFw9u9ZV8g==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1209,10 +1220,17 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + ci-info@4.0.0: resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -1311,6 +1329,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -1433,14 +1455,14 @@ packages: vue-eslint-parser: optional: true - eslint-plugin-petal@3.0.0: - resolution: {integrity: sha512-wxCbDKOry1GD9X8XpYoZ4aaZy+eG7BrRNUdeizqxJEgvmXgQI1BlRZXttnh5pQiEi0aRB5IgwIlsozaImXTQkw==} + eslint-plugin-petal@3.0.1: + resolution: {integrity: sha512-ATsPBdg4fP2pckXzvB9ZsqFA/6rLxqc4FS7XaUIu5wlR8j1q+6mOatC3P2arBIet5MMJ2bQdvEVimVa7Rc6AGA==} engines: {node: '>=20'} peerDependencies: eslint: '*' - eslint-plugin-solid@0.13.2: - resolution: {integrity: sha512-Ndeuc6MEO11YVqzIzxtKKEG0t4HPJ4ob/WQyduwpTW//NtyZqWx/gPii5GF656DLqBbsfsmMaX0zkFQ8ElyOdQ==} + eslint-plugin-solid@0.14.0: + resolution: {integrity: sha512-EY0GJLOZdLynAyBM39WBLJH2bUftGI0KekyzpfxQEhCTuOhXkIRf648P97mQjME3TDaQQbY+ObVXeE2mjfs4FQ==} engines: {node: '>=12.0.0'} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1592,6 +1614,10 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1621,6 +1647,10 @@ packages: get-tsconfig@4.7.3: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} + giget@1.2.3: + resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1640,8 +1670,8 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - globals@15.0.0: - resolution: {integrity: sha512-m/C/yR4mjO6pXDTm9/R/SpYTAIyaUB4EOzcaaMEl7mds7Mshct9GfejiJNQGjHHbdMPey13Kpu4TMbYi9ex1pw==} + globals@15.1.0: + resolution: {integrity: sha512-926gJqg+4mkxwYKiFvoomM4J0kWESfk3qfTvRL2/oc/tK/eTDBbrfcKnSa2KtfdxB5onoL7D3A3qIHQFpd4+UA==} engines: {node: '>=18'} globby@11.1.0: @@ -1716,8 +1746,8 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inline-style-parser@0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + inline-style-parser@0.2.3: + resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -1839,8 +1869,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - known-css-properties@0.24.0: - resolution: {integrity: sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==} + known-css-properties@0.30.0: + resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -1937,6 +1967,23 @@ packages: resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.6.1: resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} @@ -1984,9 +2031,17 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nypm@0.3.8: + resolution: {integrity: sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + ofetch@1.3.4: resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} + ohash@1.1.3: + resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2139,6 +2194,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -2313,8 +2371,8 @@ packages: strip-literal@2.1.0: resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} - style-to-object@0.3.0: - resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} + style-to-object@1.0.6: + resolution: {integrity: sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==} supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -2340,6 +2398,10 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2580,8 +2642,8 @@ packages: resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} engines: {node: ^14.17.0 || >=16.0.0} - yaml@2.4.1: - resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} + yaml@2.4.2: + resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} engines: {node: '>= 14'} hasBin: true @@ -2911,7 +2973,7 @@ snapshots: '@eslint/js@8.57.0': {} - '@flowr/eslint-config@3.0.0(@unocss/eslint-plugin@0.59.4(eslint@8.57.0)(typescript@5.4.5))(@vue/compiler-sfc@3.4.25)(eslint-plugin-solid@0.13.2(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(vitest@1.5.2(@types/node@20.12.7)(sass@1.75.0))': + '@flowr/eslint-config@3.0.1(@unocss/eslint-plugin@0.59.4(eslint@8.57.0)(typescript@5.4.5))(@vue/compiler-sfc@3.4.25)(eslint-plugin-solid@0.14.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(vitest@1.5.2(@types/node@20.12.7)(sass@1.75.0))': dependencies: '@antfu/install-pkg': 0.3.3 '@stylistic/eslint-plugin': 1.7.2(eslint@8.57.0)(typescript@5.4.5) @@ -2930,7 +2992,7 @@ snapshots: eslint-plugin-n: 17.3.1(eslint@8.57.0) eslint-plugin-no-only-tests: 3.1.0 eslint-plugin-perfectionist: 2.10.0(eslint@8.57.0)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@8.57.0)) - eslint-plugin-petal: 3.0.0(eslint@8.57.0) + eslint-plugin-petal: 3.0.1(eslint@8.57.0) eslint-plugin-toml: 0.11.0(eslint@8.57.0) eslint-plugin-unicorn: 52.0.0(eslint@8.57.0) eslint-plugin-unused-imports: 3.1.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) @@ -2938,7 +3000,7 @@ snapshots: eslint-plugin-vue: 9.25.0(eslint@8.57.0) eslint-plugin-yml: 1.14.0(eslint@8.57.0) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.25)(eslint@8.57.0) - globals: 15.0.0 + globals: 15.1.0 jsonc-eslint-parser: 2.4.0 local-pkg: 0.5.0 prompts: 2.4.2 @@ -2948,7 +3010,7 @@ snapshots: yaml-eslint-parser: 1.2.2 optionalDependencies: '@unocss/eslint-plugin': 0.59.4(eslint@8.57.0)(typescript@5.4.5) - eslint-plugin-solid: 0.13.2(eslint@8.57.0)(typescript@5.4.5) + eslint-plugin-solid: 0.14.0(eslint@8.57.0)(typescript@5.4.5) transitivePeerDependencies: - '@vue/compiler-sfc' - supports-color @@ -3744,6 +3806,32 @@ snapshots: builtin-modules@3.3.0: {} + bumpp@9.4.0: + dependencies: + '@jsdevtools/ez-spawn': 3.0.4 + c12: 1.10.0 + cac: 6.7.14 + escalade: 3.1.2 + fast-glob: 3.3.2 + js-yaml: 4.1.0 + prompts: 2.4.2 + semver: 7.6.0 + + c12@1.10.0: + dependencies: + chokidar: 3.6.0 + confbox: 0.1.7 + defu: 6.1.4 + dotenv: 16.4.5 + giget: 1.2.3 + jiti: 1.21.0 + mlly: 1.6.1 + ohash: 1.1.3 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.1.0 + rc9: 2.1.2 + cac@6.7.14: {} call-me-maybe@1.0.2: {} @@ -3795,8 +3883,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@2.0.0: {} + ci-info@4.0.0: {} + citty@0.1.6: + dependencies: + consola: 3.2.3 + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -3872,6 +3966,8 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv@16.4.5: {} + duplexer@0.1.2: {} electron-to-chromium@1.4.745: {} @@ -4018,7 +4114,7 @@ snapshots: eslint: 8.57.0 eslint-plugin-es-x: 7.6.0(eslint@8.57.0) get-tsconfig: 4.7.3 - globals: 15.0.0 + globals: 15.1.0 ignore: 5.3.1 minimatch: 9.0.4 semver: 7.6.0 @@ -4037,19 +4133,19 @@ snapshots: - supports-color - typescript - eslint-plugin-petal@3.0.0(eslint@8.57.0): + eslint-plugin-petal@3.0.1(eslint@8.57.0): dependencies: eslint: 8.57.0 - eslint-plugin-solid@0.13.2(eslint@8.57.0)(typescript@5.4.5): + eslint-plugin-solid@0.14.0(eslint@8.57.0)(typescript@5.4.5): dependencies: - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5) eslint: 8.57.0 estraverse: 5.3.0 is-html: 2.0.0 kebab-case: 1.0.2 - known-css-properties: 0.24.0 - style-to-object: 0.3.0 + known-css-properties: 0.30.0 + style-to-object: 1.0.6 transitivePeerDependencies: - supports-color - typescript @@ -4284,6 +4380,10 @@ snapshots: flatted@3.3.1: {} + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -4303,6 +4403,17 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + giget@1.2.3: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + defu: 6.1.4 + node-fetch-native: 1.6.4 + nypm: 0.3.8 + ohash: 1.1.3 + pathe: 1.1.2 + tar: 6.2.1 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4326,7 +4437,7 @@ snapshots: dependencies: type-fest: 0.20.2 - globals@15.0.0: {} + globals@15.1.0: {} globby@11.1.0: dependencies: @@ -4385,7 +4496,7 @@ snapshots: inherits@2.0.4: {} - inline-style-parser@0.1.1: {} + inline-style-parser@0.2.3: {} is-alphabetical@1.0.4: {} @@ -4477,7 +4588,7 @@ snapshots: kleur@3.0.3: {} - known-css-properties@0.24.0: {} + known-css-properties@0.30.0: {} kolorist@1.8.0: {} @@ -4577,6 +4688,19 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + mlly@1.6.1: dependencies: acorn: 8.11.3 @@ -4621,12 +4745,22 @@ snapshots: dependencies: boolbase: 1.0.0 + nypm@0.3.8: + dependencies: + citty: 0.1.6 + consola: 3.2.3 + execa: 8.0.1 + pathe: 1.1.2 + ufo: 1.5.3 + ofetch@1.3.4: dependencies: destr: 2.0.3 node-fetch-native: 1.6.4 ufo: 1.5.3 + ohash@1.1.3: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4769,6 +4903,11 @@ snapshots: queue-microtask@1.2.3: {} + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.3 + react-is@18.3.1: {} read-pkg-up@7.0.1: @@ -4965,9 +5104,9 @@ snapshots: dependencies: js-tokens: 9.0.0 - style-to-object@0.3.0: + style-to-object@1.0.6: dependencies: - inline-style-parser: 0.1.1 + inline-style-parser: 0.2.3 supports-color@5.5.0: dependencies: @@ -4991,6 +5130,15 @@ snapshots: tapable@2.2.1: {} + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + text-table@0.2.0: {} tinybench@2.8.0: {} @@ -5225,9 +5373,9 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 lodash: 4.17.21 - yaml: 2.4.1 + yaml: 2.4.2 - yaml@2.4.1: {} + yaml@2.4.2: {} yocto-queue@0.1.0: {} diff --git a/testing/src/main.rs b/testing/src/main.rs index c2148f17..e9c1f95f 100644 --- a/testing/src/main.rs +++ b/testing/src/main.rs @@ -15,7 +15,7 @@ async fn main() -> onelauncher::Result<()> { let login = minecraft::begin().await?; println!("{}", login.redirect_uri.as_str()); - + let mut input = String::new(); std::io::stdin() .read_line(&mut input) @@ -31,6 +31,7 @@ async fn main() -> onelauncher::Result<()> { // test changing fetch settings and resetting the semaphore state.reset_fetch_semaphore().await; + println!("clearing clusters"); { let c = cluster::list(None).await?; for (id, _) in c.into_iter() { @@ -43,7 +44,37 @@ async fn main() -> onelauncher::Result<()> { let loader = Loader::Vanilla; let loader_version = "stable".to_string(); + let cluster = create::create_cluster( + name.clone(), + game, + loader, + Some(loader_version), + None, + None, + None, + None, + None, + ) + .await?; + State::sync().await?; + println!("running minecraft"); + let c_lock = cluster::run(&cluster).await?; + let uuid = c_lock.read().await.uuid; + let pid = c_lock.read().await.current_child.read().await.id(); + + println!("mc_uuid: {}", uuid); + println!("mc_pid: {:?}", pid); + + println!("proc_uuid: {:?}", processor::get_running().await?); + println!( + "proc_path: {:?}", + processor::get_running_cluster_paths().await? + ); + + let mut proc = c_lock.write().await; + processor::wait_for(&mut proc).await?; + Ok(()) }