🚧 wip: it launches (sort of), temp launching/cluster utils, format, relicense, refactor backend into polyfrost/infrastructure
This commit is contained in:
parent
e306d45e0a
commit
8c49553473
72 changed files with 6411 additions and 3818 deletions
|
|
@ -1,8 +0,0 @@
|
|||
.vscode
|
||||
target
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
.github
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ members = [
|
|||
"debugger",
|
||||
"desktop/src-tauri",
|
||||
"testing",
|
||||
"backend"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
30
Dockerfile
30
Dockerfile
|
|
@ -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"]
|
||||
144
LICENSE
144
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. <https://fsf.org/>
|
||||
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) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
@ -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<state::AppState>,
|
||||
path: Path<(String, String)>,
|
||||
) -> impl Responder {
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
icon: Option<PathBuf>,
|
||||
icon_url: Option<String>,
|
||||
package_data: Option<PackageData>,
|
||||
skip: Option<bool>,
|
||||
skip_watch: Option<bool>,
|
||||
) -> crate::Result<ClusterPath> {
|
||||
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<ClusterPath> {
|
||||
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<ClusterPath> {
|
||||
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<String>,
|
||||
) -> crate::Result<Option<LoaderVersion>> {
|
||||
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),
|
||||
}
|
||||
|
|
@ -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<bool>) -> crate::Result<Option<Cluster>> {
|
||||
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<bool>) -> crate::Result<HashMap<ClusterPath, Cluster>> {
|
||||
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<Arc<RwLock<ProcessorChild>>> {
|
||||
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<Arc<RwLock<ProcessorChild>>> {
|
||||
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::<Vec<&str>>())
|
||||
.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<bool>,
|
||||
) -> crate::Result<Option<Cluster>> {
|
||||
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<bool>) -> crate::Result<Option<Cluster>> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<FutFn>(
|
||||
path: &ClusterPath,
|
||||
action: impl Fn(&mut Cluster) -> FutFn,
|
||||
path: &ClusterPath,
|
||||
action: impl Fn(&mut Cluster) -> FutFn,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
FutFn: Future<Output = crate::Result<()>>,
|
||||
FutFn: Future<Output = 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;
|
||||
|
||||
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<Option<JavaVersion>> {
|
||||
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;
|
||||
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
|
||||
*/
|
||||
/*
|
||||
* todo
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
State::sync().await?;
|
||||
State::sync().await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sanitize a user-inputted [`Cluster`] name.
|
||||
pub fn sanitize_cluster_name(input: &str) -> String {
|
||||
input.replace(['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>'], "_")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
//! Utilities for updating Clusters
|
||||
|
|
@ -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<InternetPayload> {
|
||||
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<InternetPayload> {
|
||||
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());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::store::{IngressProcessType, IngressProcessor};
|
||||
|
||||
pub async fn check_ingress_feeds() -> crate::Result<bool> {
|
||||
IngressProcessor::finished(IngressProcessType::IngressFeed).await
|
||||
IngressProcessor::finished(IngressProcessType::IngressFeed).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u32>,
|
||||
) -> crate::Result<Vec<JavaVersion>> {
|
||||
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<u32>) -> crate::Result<Vec<JavaVersion>> {
|
||||
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<PathBuf> {
|
||||
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::<Vec<Package>>(Method::GET, &format!(
|
||||
send_ingress(&ingress, 0.0, Some("fetching java api")).await?;
|
||||
let packages = fetch_json::<Vec<Package>>(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<Option<JavaVersion>> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<u64> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<LogOutput>
|
||||
/// The log file to read as a [`String`]
|
||||
pub log_file: String,
|
||||
/// The parsed and censored output of the logfile.
|
||||
pub output: Option<LogOutput>,
|
||||
}
|
||||
|
||||
impl LogManager {
|
||||
/// Initialize a new [`LogManager`].
|
||||
async fn initialize(cluster: &ClusterPath, log_file: String, clear: Option<bool>) -> crate::Result<Self> {
|
||||
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<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
output: if clear.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(get_output_by_file(cluster, &log_file).await?)
|
||||
},
|
||||
log_file,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a [`Vec<LogManager>`] for a specific [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs(cluster_path: ClusterPath, clear: Option<bool>) -> crate::Result<Vec<LogManager>> {
|
||||
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<bool>,
|
||||
) -> crate::Result<Vec<LogManager>> {
|
||||
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::<crate::Result<Vec<LogManager>>>()?;
|
||||
logs.sort_by_key(|x| x.log_file.clone());
|
||||
Ok(logs)
|
||||
let mut logs = logs
|
||||
.into_iter()
|
||||
.collect::<crate::Result<Vec<LogManager>>>()?;
|
||||
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<LogManager> {
|
||||
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<LogManager> {
|
||||
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<LogCursor> {
|
||||
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<LogCursor> {
|
||||
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<LogCursor> {
|
||||
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<MinecraftCredentials> = 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<MinecraftCredentials> = 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<LogOutput> {
|
||||
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<MinecraftCredentials> = state.users.read().await.users.clone().into_values().collect();
|
||||
pub async fn get_output_by_file(
|
||||
cluster_path: &ClusterPath,
|
||||
log_file: &str,
|
||||
) -> crate::Result<LogOutput> {
|
||||
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<MinecraftCredentials> = 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<MinecraftCredentials>, credentials_store: Option<Credentials>) -> 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<MinecraftCredentials>,
|
||||
credentials_store: Option<Credentials>,
|
||||
) -> 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<VersionManifest> {
|
||||
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<Manifest> {
|
||||
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<Manifest> {
|
||||
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<Manifest> {
|
||||
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<Manifest> {
|
||||
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<Manifest> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,73 +5,76 @@ use crate::store::{MinecraftCredentials, MinecraftLogin, State};
|
|||
/// Begin a Microsoft authentication flow.
|
||||
#[tracing::instrument]
|
||||
pub async fn begin() -> crate::Result<MinecraftLogin> {
|
||||
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<MinecraftCredentials> {
|
||||
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<Option<uuid::Uuid>> {
|
||||
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;
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.remove(user).await?;
|
||||
users.remove(user).await?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Store this securely in stronghold
|
||||
/// Get a list of user [`MinecraftCredentials`].
|
||||
#[tracing::instrument]
|
||||
pub async fn users() -> crate::Result<Vec<MinecraftCredentials>> {
|
||||
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<MinecraftCredentials> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,32 @@
|
|||
//!
|
||||
//! 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};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub icon: Option<PathBuf>,
|
||||
pub icon_url: Option<String>,
|
||||
pub package_data: Option<PackageData>,
|
||||
pub skip: Option<bool>,
|
||||
pub skip_watch: Option<bool>,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
core/src/api/package/import/ftb.rs
Normal file
1
core/src/api/package/import/ftb.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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<Vec<String>> {
|
||||
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!("importing instance from {instance_path}");
|
||||
|
||||
tracing::debug!("completed import of instance.");
|
||||
Ok(())
|
||||
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<PathBuf> {
|
||||
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<Option<PathBuf>> {
|
||||
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<IngressId>,
|
||||
) -> crate::Result<IngressId> {
|
||||
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<PathBuf>`] 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<Vec<PathBuf>> {
|
||||
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?);
|
||||
}
|
||||
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)
|
||||
Ok(files)
|
||||
}
|
||||
|
|
|
|||
1
core/src/api/package/import/modrinth.rs
Normal file
1
core/src/api/package/import/modrinth.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
core/src/api/package/import/technic.rs
Normal file
1
core/src/api/package/import/technic.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
core/src/api/package/import/tlauncher.rs
Normal file
1
core/src/api/package/import/tlauncher.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
//! Import and Export `.mrpack` mod packs.
|
||||
|
|
@ -0,0 +1 @@
|
|||
//! Import and Export `.zip` mod packs
|
||||
|
|
@ -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<bool> {
|
||||
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<Option<i32>> {
|
||||
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<Vec<Uuid>> {
|
||||
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<Vec<Uuid>> {
|
||||
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<Vec<ClusterPath>>
|
||||
{
|
||||
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<Vec<ClusterPath>> {
|
||||
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<Vec<Cluster>> {
|
||||
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<Vec<Uuid>> {
|
||||
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<Vec<Uuid>> {
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Arc<ProxyState>> = OnceCell::const_new();
|
||||
pub struct ProxyState {
|
||||
#[cfg(feature = "tauri")]
|
||||
pub app: tauri::AppHandle,
|
||||
pub ingress_feeds: RwLock<HashMap<Uuid, Ingress>>,
|
||||
#[cfg(feature = "tauri")]
|
||||
pub app: tauri::AppHandle,
|
||||
pub ingress_feeds: RwLock<HashMap<Uuid, Ingress>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Ingress {
|
||||
pub ingress_uuid: Uuid,
|
||||
pub message: String,
|
||||
pub ingress_type: IngressType,
|
||||
pub total: f64,
|
||||
pub current: f64,
|
||||
#[serde(skip)]
|
||||
pub last_sent: f64,
|
||||
#[cfg(feature = "cli")]
|
||||
#[serde(skip)]
|
||||
pub cli: indicatif::ProgressBar,
|
||||
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<Arc<Self>> {
|
||||
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<Arc<Self>> {
|
||||
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<Arc<Self>> {
|
||||
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<Arc<Self>> {
|
||||
PROXY_STATE
|
||||
.get_or_try_init(|| async {
|
||||
Ok(Arc::new(Self {
|
||||
ingress_feeds: RwLock::new(HashMap::new()),
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn get_main_window() -> crate::Result<tauri::WebviewWindow> {
|
||||
use tauri::Manager;
|
||||
let value = Self::get().await?;
|
||||
Ok(value.app.get_webview_window("main").unwrap())
|
||||
}
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn get_main_window() -> crate::Result<tauri::WebviewWindow> {
|
||||
use tauri::Manager;
|
||||
let value = Self::get().await?;
|
||||
Ok(value.app.get_webview_window("main").unwrap())
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
|
||||
}
|
||||
#[cfg(feature = "tauri")]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
Ok(PROXY_STATE.get().ok_or(ProxyError::NotInitialized)?.clone())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
Self::initialize().await
|
||||
}
|
||||
#[cfg(not(feature = "tauri"))]
|
||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||
Self::initialize().await
|
||||
}
|
||||
|
||||
pub async fn list_ingress_feeds() -> crate::Result<HashMap<Uuid, Ingress>> {
|
||||
let value = Self::get().await?;
|
||||
let read = value.ingress_feeds.read().await;
|
||||
pub async fn list_ingress_feeds() -> crate::Result<HashMap<Uuid, Ingress>> {
|
||||
let value = Self::get().await?;
|
||||
let read = value.ingress_feeds.read().await;
|
||||
|
||||
let mut display_list: HashMap<Uuid, Ingress> = HashMap::new();
|
||||
for (uuid, ingress) in read.iter() {
|
||||
display_list.insert(*uuid, ingress.clone());
|
||||
}
|
||||
let mut display_list: HashMap<Uuid, Ingress> = 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<PathBuf>,
|
||||
package_id: Option<String>,
|
||||
package_version: Option<String>
|
||||
}
|
||||
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<PathBuf>,
|
||||
package_id: Option<String>,
|
||||
package_version: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct IngressPayload {
|
||||
pub event: IngressType,
|
||||
pub ingress_uuid: Uuid,
|
||||
pub fraction: Option<f64>,
|
||||
pub message: String,
|
||||
pub event: IngressType,
|
||||
pub ingress_uuid: Uuid,
|
||||
pub fraction: Option<f64>,
|
||||
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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IngressId> {
|
||||
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<IngressId> {
|
||||
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<IngressId>,
|
||||
ingress_type: IngressType,
|
||||
total: f64,
|
||||
title: &str,
|
||||
id: Option<IngressId>,
|
||||
ingress_type: IngressType,
|
||||
total: f64,
|
||||
title: &str,
|
||||
) -> crate::Result<IngressId> {
|
||||
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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,31 +47,32 @@ macro_rules! ingress_join {
|
|||
#[tracing::instrument(skip(stream, f))]
|
||||
#[onelauncher_debug::debugger]
|
||||
pub async fn ingress_try_for_each<I, F, Fut, T>(
|
||||
stream: I,
|
||||
limit: Option<usize>,
|
||||
key: Option<&IngressId>,
|
||||
total: f64,
|
||||
num_futs: usize,
|
||||
message: Option<&str>,
|
||||
f: F,
|
||||
stream: I,
|
||||
limit: Option<usize>,
|
||||
key: Option<&IngressId>,
|
||||
total: f64,
|
||||
num_futs: usize,
|
||||
message: Option<&str>,
|
||||
f: F,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
|
||||
F: FnMut(T) -> Fut + Send,
|
||||
Fut: Future<Output = crate::Result<()>> + Send,
|
||||
T: Send,
|
||||
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
|
||||
F: FnMut(T) -> Fut + Send,
|
||||
Fut: Future<Output = crate::Result<()>> + 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Settings> {
|
||||
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::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::info!(
|
||||
"successfully switched configuration directory to {}",
|
||||
new.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
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<bool> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
403
core/src/game/arguments.rs
Normal file
403
core/src/game/arguments.rs
Normal file
|
|
@ -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<String>,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
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<Vec<String>> {
|
||||
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<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
args: &[T],
|
||||
data: &HashMap<String, SidedDataEntry>,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
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<Option<String>> {
|
||||
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::<Option<String>, 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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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::<Result<HashSet<_>, _>>()?;
|
||||
|
||||
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::<Vec<_>>()
|
||||
.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_classpath_library<T: AsRef<str>>(
|
||||
libraries_path: &Path,
|
||||
libraries: &[T],
|
||||
java_arch: &str,
|
||||
) -> crate::Result<String> {
|
||||
let classpaths = libraries
|
||||
.iter()
|
||||
.map(|lib| get_library(libraries_path, lib.as_ref(), false))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(classpaths.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_library(
|
||||
libraries_path: &Path,
|
||||
library: &str,
|
||||
error_exist: bool,
|
||||
) -> crate::Result<String> {
|
||||
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<ParseFn>(
|
||||
args: &[Argument],
|
||||
parsed: &mut Vec<String>,
|
||||
parse_function: ParseFn,
|
||||
java_arch: &str,
|
||||
) -> crate::Result<()>
|
||||
where
|
||||
ParseFn: Fn(&str) -> crate::Result<String>,
|
||||
{
|
||||
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(())
|
||||
}
|
||||
328
core/src/game/metadata.rs
Normal file
328
core/src/game/metadata.rs
Normal file
|
|
@ -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<bool>,
|
||||
ingress: Option<&IngressId>,
|
||||
) -> crate::Result<GameVersionInfo> {
|
||||
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::<crate::Error>()
|
||||
.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<AssetsIndex> {
|
||||
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::<crate::Error>()
|
||||
.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::<bytes::Bytes>::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(())
|
||||
}
|
||||
552
core/src/game/mod.rs
Normal file
552
core/src/game/mod.rs
Normal file
|
|
@ -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<IngressId>,
|
||||
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<String>,
|
||||
credentials: &MinecraftCredentials,
|
||||
resolution: &st::Resolution,
|
||||
memory: &st::Memory,
|
||||
wrapper: &Option<String>,
|
||||
) -> crate::Result<Arc<tokio::sync::RwLock<ProcessorChild>>> {
|
||||
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::<Vec<_>>(),
|
||||
)
|
||||
.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::<Vec<_>>(),
|
||||
)
|
||||
.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::<Vec<Option<bool>>>();
|
||||
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<bool> {
|
||||
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<Option<JavaVersion>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<P: AsRef<Path>>(
|
||||
path: P,
|
||||
password: Zeroizing<Vec<u8>>,
|
||||
dirs: &Directories,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
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<P: AsRef<Path>>(
|
||||
path: P,
|
||||
password: Zeroizing<Vec<u8>>,
|
||||
dirs: &Directories,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>,
|
||||
/// 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<PathBuf>,
|
||||
/// 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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
let settings_dir = Self::init_settings_dir().ok_or(DirectoryError::ConfigDir)?;
|
||||
Ok(settings_dir.join("settings.json"))
|
||||
}
|
||||
|
||||
/// Initialize the core directory manager
|
||||
#[tracing::instrument]
|
||||
pub fn initalize(settings: &Settings) -> crate::Result<Self> {
|
||||
let cwd = std::env::current_dir().map_err(|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<Self> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
Self::init_settings_dir().map(|d| d.join("logs"))
|
||||
}
|
||||
|
||||
/// Get the `settings_dir/settings.json` file as a [`PathBuf`].
|
||||
#[inline]
|
||||
pub fn settings_file(&self) -> PathBuf {
|
||||
self.settings_dir.join(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<PathBuf> {
|
||||
std::env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
/// Get a [`PathBuf`] from a provided environment variable.
|
||||
#[inline]
|
||||
fn env_path(name: &str) -> Option<PathBuf> {
|
||||
std::env::var_os(name).map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/assets` directory.
|
||||
#[inline]
|
||||
pub async fn assets_dir(&self) -> PathBuf {
|
||||
self.metadata_dir().await.join("assets")
|
||||
}
|
||||
/// Get the `config_dir/metadata/assets` 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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<RwLock<DiscordIpcClient>>,
|
||||
/// Whether or not we are connected to the IPC server and internet.
|
||||
connected: Arc<AtomicBool>,
|
||||
/// The managed [`DiscordIpcClient`] used for this IPC client.
|
||||
client: Arc<RwLock<DiscordIpcClient>>,
|
||||
/// Whether or not we are connected to the IPC server and internet.
|
||||
connected: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<dyn serde::ser::StdError>| {
|
||||
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<dyn serde::ser::StdError>| {
|
||||
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(()); }
|
||||
/// 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<dyn serde::ser::StdError>| {
|
||||
anyhow::anyhow!("failed to clear discord activity {}", e)
|
||||
};
|
||||
let mut client = self.client.write().await;
|
||||
let result = client.clear_activity();
|
||||
let could_not_clear = |e: Box<dyn serde::ser::StdError>| {
|
||||
anyhow::anyhow!("failed to clear discord activity {}", e)
|
||||
};
|
||||
|
||||
if reconnect_if_fail {
|
||||
if result.is_err() {
|
||||
client.reconnect().map_err(|e| {
|
||||
anyhow::anyhow!("failed to reconnect to Discord RPC {}", e)
|
||||
})?;
|
||||
if reconnect_if_fail {
|
||||
if result.is_err() {
|
||||
client
|
||||
.reconnect()
|
||||
.map_err(|e| anyhow::anyhow!("failed to reconnect to Discord RPC {}", e))?;
|
||||
|
||||
return Ok(client.clear_activity().map_err(could_not_clear)?);
|
||||
}
|
||||
} else {
|
||||
result.map_err(could_not_clear)?;
|
||||
}
|
||||
return Ok(client.clear_activity().map_err(could_not_clear)?);
|
||||
}
|
||||
} else {
|
||||
result.map_err(could_not_clear)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear and disable the Discord IPC, reconnecting if requested.
|
||||
pub async fn clear(&self, reconnect_if_fail: bool) -> crate::Result<()> {
|
||||
let state: Arc<tokio::sync::RwLockReadGuard<'_, State>> = State::get().await?;
|
||||
/// Clear and disable the Discord IPC, reconnecting if requested.
|
||||
pub async fn clear(&self, reconnect_if_fail: bool) -> crate::Result<()> {
|
||||
let state: Arc<tokio::sync::RwLockReadGuard<'_, State>> = State::get().await?;
|
||||
|
||||
{
|
||||
let settings = state.settings.read().await;
|
||||
if settings.disable_discord {
|
||||
println!("discord is disabled.. clearing");
|
||||
return self.clear_activity(true).await;
|
||||
}
|
||||
}
|
||||
{
|
||||
let settings = state.settings.read().await;
|
||||
if settings.disable_discord {
|
||||
println!("discord is disabled.. clearing");
|
||||
return self.clear_activity(true).await;
|
||||
}
|
||||
}
|
||||
|
||||
self.set_activity("Idling...", reconnect_if_fail).await?;
|
||||
self.set_activity("Idling...", reconnect_if_fail).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
//! Global Package Metadata management.
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, JavaVersion>);
|
||||
|
||||
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<String> {
|
||||
self.0.keys().cloned().collect()
|
||||
}
|
||||
/// A collection visiting all keys in arbitrary order.
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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::<Metadata>(&path, io_semaphore).await {
|
||||
metadata = Some(metadata_json);
|
||||
} else if online {
|
||||
let res = async {
|
||||
let fetch_data = Self::fetch().await?;
|
||||
if let Ok(metadata_json) = read_json::<Metadata>(&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?;
|
||||
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?;
|
||||
|
||||
metadata = Some(fetch_data);
|
||||
Ok::<(), crate::Error>(())
|
||||
}.await;
|
||||
metadata = Some(fetch_data);
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => { tracing::warn!("failed to fetch metadata: {err}") }
|
||||
}
|
||||
} else if let Ok(metadata_json) = read_json::<Metadata>(&backup, io_semaphore).await {
|
||||
metadata = Some(metadata_json);
|
||||
copy(&backup, &path).await?;
|
||||
}
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to fetch metadata: {err}")
|
||||
}
|
||||
}
|
||||
} else if let Ok(metadata_json) = read_json::<Metadata>(&backup, io_semaphore).await {
|
||||
metadata = Some(metadata_json);
|
||||
copy(&backup, &path).await?;
|
||||
}
|
||||
|
||||
if let Some(meta) = metadata {
|
||||
Ok(meta)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("failed to fetch launcher metadata").into())
|
||||
}
|
||||
}
|
||||
if let Some(meta) = metadata {
|
||||
Ok(meta)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("failed to fetch launcher metadata").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Update and backup all available metadata.
|
||||
pub async fn update() {
|
||||
let res = async {
|
||||
let fetch_data = Metadata::fetch().await?;
|
||||
let state = State::get().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?;
|
||||
|
||||
let path = state
|
||||
.directories
|
||||
.caches_dir()
|
||||
.await
|
||||
.join("metadata.json");
|
||||
let backup = state
|
||||
.directories
|
||||
.caches_dir()
|
||||
.await
|
||||
.join("metadata.json.bak");
|
||||
let path = state.directories.caches_dir().await.join("metadata.json");
|
||||
let backup = state
|
||||
.directories
|
||||
.caches_dir()
|
||||
.await
|
||||
.join("metadata.json.bak");
|
||||
|
||||
if path.exists() {
|
||||
copy(&path, &backup).await?;
|
||||
}
|
||||
if path.exists() {
|
||||
copy(&path, &backup).await?;
|
||||
}
|
||||
|
||||
write(&path, &serde_json::to_vec(&fetch_data)?, &state.io_semaphore).await?;
|
||||
write(
|
||||
&path,
|
||||
&serde_json::to_vec(&fetch_data)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut old_metadata = state.metadata.write().await;
|
||||
*old_metadata = fetch_data;
|
||||
let mut old_metadata = state.metadata.write().await;
|
||||
*old_metadata = fetch_data;
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}.await;
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to update launcher metadata: {err}")
|
||||
}
|
||||
};
|
||||
}
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to update launcher metadata: {err}")
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self> {
|
||||
pub async fn initialize(
|
||||
dirs: &super::Directories,
|
||||
io_semaphore: &crate::utils::http::IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
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<MinecraftCredentials> {
|
||||
pub async fn finish(
|
||||
&mut self,
|
||||
code: &str,
|
||||
flow: MinecraftLogin,
|
||||
) -> crate::Result<MinecraftCredentials> {
|
||||
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<DeviceToken, MinecraftAuthE
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::DeviceToken,
|
||||
).await?.1)
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -352,7 +372,8 @@ async fn sisu_authenticate(
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::SisuAuthenicate,
|
||||
).await?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session_id = headers
|
||||
.get("X-SessionId")
|
||||
|
|
@ -397,12 +418,19 @@ async fn oauth_token(code: &str, verify: &str) -> Result<OAuthToken, MinecraftAu
|
|||
})?;
|
||||
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthError::RequestError { step: MinecraftAuthStep::OAuthToken, source }
|
||||
})?;
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::OAuthToken,
|
||||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthError::DeserializeError { step: MinecraftAuthStep::OAuthToken, raw: text, source, status_code: status }
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::OAuthToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -422,15 +450,25 @@ async fn oauth_refresh(refresh_token: &str) -> Result<OAuthToken, MinecraftAuthE
|
|||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::RefreshOAuthToken, source })?;
|
||||
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthError::RequestError { step: MinecraftAuthStep::RefreshOAuthToken, source }
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthError::DeserializeError { step: MinecraftAuthStep::RefreshOAuthToken, raw: text, source, status_code: status }
|
||||
let status = res.status();
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -477,7 +515,9 @@ async fn sisu_authorize(
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::SisuAuthorize,
|
||||
).await?.1)
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
|
|
@ -502,7 +542,9 @@ async fn xsts_authorize(
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::XstsAuthorize,
|
||||
).await?.1)
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -533,14 +575,27 @@ async fn minecraft_token(token: DeviceToken) -> Result<MinecraftToken, Minecraft
|
|||
"xtoken": format!("XBL3.0 x={uhs};{token}"),
|
||||
}))
|
||||
.send()
|
||||
}).await
|
||||
.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::MinecraftToken, source })?;
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
source,
|
||||
})?;
|
||||
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::MinecraftToken, source })?;
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthError::DeserializeError { step: MinecraftAuthStep::MinecraftToken, raw: text, source, status_code: status }
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -560,13 +615,25 @@ async fn minecraft_profile(token: &str) -> Result<MinecraftProfile, MinecraftAut
|
|||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::MinecraftProfile, source })?;
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
source,
|
||||
})?;
|
||||
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::MinecraftProfile, source })?;
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthError::DeserializeError { step: MinecraftAuthStep::MinecraftProfile, raw: text, source, status_code: status }
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -584,20 +651,30 @@ async fn minecraft_entitlements(token: &str) -> Result<MinecraftEntitlements, Mi
|
|||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::MinecraftEntitlements, source })?;
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||
source,
|
||||
})?;
|
||||
|
||||
let status = res.status();
|
||||
let text = res.text().await.map_err(|source| MinecraftAuthError::RequestError { step: MinecraftAuthStep::MinecraftEntitlements, source })?;
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthError::RequestError {
|
||||
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthError::DeserializeError { step: MinecraftAuthStep::MinecraftEntitlements, raw: text, source, status_code: status }
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(request_fn))]
|
||||
async fn auth_retry<F>(
|
||||
request_fn: impl Fn() -> F,
|
||||
) -> Result<reqwest::Response, reqwest::Error>
|
||||
async fn auth_retry<F>(request_fn: impl Fn() -> F) -> Result<reqwest::Response, reqwest::Error>
|
||||
where
|
||||
F: std::future::Future<Output = Result<reqwest::Response, reqwest::Error>>,
|
||||
{
|
||||
|
|
@ -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<DeviceKey, MinecraftAuthError> {
|
|||
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<T: serde::de::DeserializeOwned>(
|
|||
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::<byteorder::BigEndian>(1).map_err(|source| {
|
||||
MinecraftAuthError::SigningError { step, source }
|
||||
})?;
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthError::SigningError { step, source }
|
||||
})?;
|
||||
buffer.write_u64::<byteorder::BigEndian>(time as u64).map_err(|source| {
|
||||
MinecraftAuthError::SigningError { step, source }
|
||||
})?;
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthError::SigningError { step, source }
|
||||
})?;
|
||||
buffer
|
||||
.write_u32::<byteorder::BigEndian>(1)
|
||||
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
|
||||
buffer
|
||||
.write_u8(0)
|
||||
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
|
||||
buffer
|
||||
.write_u64::<byteorder::BigEndian>(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::<byteorder::BigEndian>(1).map_err(|source| {
|
||||
MinecraftAuthError::SigningError { step, source }
|
||||
})?;
|
||||
sig_buffer.write_u64::<byteorder::BigEndian>(time as u64).map_err(|source| {
|
||||
MinecraftAuthError::SigningError { step, source }
|
||||
})?;
|
||||
sig_buffer
|
||||
.write_i32::<byteorder::BigEndian>(1)
|
||||
.map_err(|source| MinecraftAuthError::SigningError { step, source })?;
|
||||
sig_buffer
|
||||
.write_u64::<byteorder::BigEndian>(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<T: serde::de::DeserializeOwned>(
|
|||
}
|
||||
|
||||
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")]
|
||||
|
|
|
|||
|
|
@ -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<RwLock<State>> = OnceCell::const_new();
|
|||
|
||||
/// The public OneLauncher state structure.
|
||||
pub struct State {
|
||||
/// Handles a boolean to check if the launcher is connected to the internet
|
||||
pub offline: RwLock<bool>,
|
||||
/// Handles core launcher directory and files
|
||||
pub directories: Directories,
|
||||
/// Handles a boolean to check if the launcher is connected to the internet
|
||||
pub offline: RwLock<bool>,
|
||||
/// Handles core launcher directory and files
|
||||
pub directories: Directories,
|
||||
|
||||
/// Semaphore used to handle concurrent network requests
|
||||
pub fetch_semaphore: FetchSemaphore,
|
||||
/// Maximum number of semaphore permits to limit the amount of network requests at one time
|
||||
pub fetch_semaphore_max: RwLock<u32>,
|
||||
/// Semaphore used to handle concurrent IO requests
|
||||
pub io_semaphore: IoSemaphore,
|
||||
/// Maximum number of semaphore permits to limit the amount of IO requests at one time
|
||||
pub io_semaphore_max: RwLock<u32>,
|
||||
/// Semaphore used to handle concurrent network requests
|
||||
pub fetch_semaphore: FetchSemaphore,
|
||||
/// Maximum number of semaphore permits to limit the amount of network requests at one time
|
||||
pub fetch_semaphore_max: RwLock<u32>,
|
||||
/// Semaphore used to handle concurrent IO requests
|
||||
pub io_semaphore: IoSemaphore,
|
||||
/// Maximum number of semaphore permits to limit the amount of IO requests at one time
|
||||
pub io_semaphore_max: RwLock<u32>,
|
||||
|
||||
/// Handles launcher metadata via [`interpulse`]
|
||||
pub metadata: RwLock<Metadata>,
|
||||
/// Handles launcher configuration and settings
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Handles minecraft auth flows and users
|
||||
pub users: RwLock<MinecraftState>,
|
||||
/// Handles processes
|
||||
pub processor: RwLock<Processor>,
|
||||
/// Handles OneConfig integration with [`async-tungstenite`]
|
||||
pub oneconfig: RwLock<Option<OneConfig>>,
|
||||
/// Handles secure credential management with [`iota_stronghold`].
|
||||
pub credentials: RwLock<Option<Credentials>>,
|
||||
/// Handles clusters
|
||||
pub(crate) clusters: RwLock<Clusters>,
|
||||
/// Handles internal ingress processes
|
||||
pub(crate) ingress_processor: RwLock<IngressProcessor>,
|
||||
/// Handles file system watching for cluster manager
|
||||
pub(crate) watcher: RwLock<Debouncer<RecommendedWatcher>>,
|
||||
/// Handles Discord rich prescence
|
||||
pub discord_rpc: DiscordRPC,
|
||||
/// Handles launcher metadata via [`interpulse`]
|
||||
pub metadata: RwLock<Metadata>,
|
||||
/// Handles launcher configuration and settings
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Handles minecraft auth flows and users
|
||||
pub users: RwLock<MinecraftState>,
|
||||
/// Handles processes
|
||||
pub processor: RwLock<Processor>,
|
||||
/// Handles OneConfig integration with [`async-tungstenite`]
|
||||
pub oneconfig: RwLock<Option<OneConfig>>,
|
||||
/// Handles secure credential management with [`iota_stronghold`].
|
||||
pub credentials: RwLock<Option<Credentials>>,
|
||||
/// Handles clusters
|
||||
pub(crate) clusters: RwLock<Clusters>,
|
||||
/// Handles internal ingress processes
|
||||
pub(crate) ingress_processor: RwLock<IngressProcessor>,
|
||||
/// Handles file system watching for cluster manager
|
||||
pub(crate) watcher: RwLock<Debouncer<RecommendedWatcher>>,
|
||||
/// Handles Discord rich prescence
|
||||
pub discord_rpc: DiscordRPC,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Get the current global launcher state (or initialize it)
|
||||
pub async fn get() -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(LAUNCHER_STATE.get_or_try_init(Self::initialize).await?.read().await))
|
||||
}
|
||||
/// Get the current global launcher state (or initialize it)
|
||||
pub async fn get() -> crate::Result<Arc<tokio::sync::RwLockReadGuard<'static, Self>>> {
|
||||
Ok(Arc::new(
|
||||
LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.read()
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
/// Get and lock writing to the current global launcher state (or initalize it)
|
||||
pub async fn get_and_write() -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
|
||||
Ok(LAUNCHER_STATE.get_or_try_init(Self::initialize).await?.write().await)
|
||||
}
|
||||
/// Get and lock writing to the current global launcher state (or initalize it)
|
||||
pub async fn get_and_write() -> crate::Result<tokio::sync::RwLockWriteGuard<'static, Self>> {
|
||||
Ok(LAUNCHER_STATE
|
||||
.get_or_try_init(Self::initialize)
|
||||
.await?
|
||||
.write()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Initializes the core OneLauncher state fully.
|
||||
#[tracing::instrument]
|
||||
#[onelauncher_debug::debugger]
|
||||
async fn initialize() -> crate::Result<RwLock<State>> {
|
||||
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<RwLock<State>> {
|
||||
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::<RwLock<Self>, crate::Error>(RwLock::new(Self {
|
||||
offline: RwLock::new(is_offline),
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(settings.max_async_fetches as u32),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(settings.max_async_io_operations as u32),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
users: RwLock::new(users),
|
||||
processor: RwLock::new(processor),
|
||||
oneconfig: RwLock::new(None),
|
||||
credentials: RwLock::new(None),
|
||||
clusters: RwLock::new(clusters),
|
||||
ingress_processor: RwLock::new(ingress_processor),
|
||||
watcher: RwLock::new(watcher),
|
||||
discord_rpc,
|
||||
}))
|
||||
}
|
||||
Ok::<RwLock<Self>, crate::Error>(RwLock::new(Self {
|
||||
offline: RwLock::new(is_offline),
|
||||
directories,
|
||||
fetch_semaphore,
|
||||
fetch_semaphore_max: RwLock::new(settings.max_async_fetches as u32),
|
||||
io_semaphore,
|
||||
io_semaphore_max: RwLock::new(settings.max_async_io_operations as u32),
|
||||
metadata: RwLock::new(metadata),
|
||||
settings: RwLock::new(settings),
|
||||
users: RwLock::new(users),
|
||||
processor: RwLock::new(processor),
|
||||
oneconfig: RwLock::new(None),
|
||||
credentials: RwLock::new(None),
|
||||
clusters: RwLock::new(clusters),
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<async_tungstenite::tokio::ConnectStream>,
|
||||
}
|
||||
|
||||
/// 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<Self> {
|
||||
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<Self> {
|
||||
let (socket, _) = async_tungstenite::tokio::connect_async(format!(
|
||||
"wss://localhost:4023/oneconfig/ws?cluster={cluster}"
|
||||
))
|
||||
.await?;
|
||||
Ok(Self { socket })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
//! 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};
|
||||
|
||||
|
|
@ -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<PathBuf>,
|
||||
cache_path: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
// credentials: &Credentials,
|
||||
cluster: Cluster,
|
||||
paths: Vec<PathBuf>,
|
||||
cache_path: PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
fetch_semaphore: &FetchSemaphore,
|
||||
// credentials: &Credentials,
|
||||
) -> crate::Result<HashMap<PackagePath, Package>> {
|
||||
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<String>) -> Option<Self> {
|
||||
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<String>) -> Option<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<ManagedPackage>,
|
||||
version: Box<ManagedVersion>,
|
||||
users: Box<ManagedUser>,
|
||||
update: Option<Box<ManagedVersion>>,
|
||||
incompatible: bool,
|
||||
},
|
||||
Mapped {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
authors: Vec<String>,
|
||||
version: Option<String>,
|
||||
icon: Option<PathBuf>,
|
||||
package_type: Option<String>,
|
||||
},
|
||||
Unknown,
|
||||
Managed {
|
||||
package: Box<ManagedPackage>,
|
||||
version: Box<ManagedVersion>,
|
||||
users: Box<ManagedUser>,
|
||||
update: Option<Box<ManagedVersion>>,
|
||||
incompatible: bool,
|
||||
},
|
||||
Mapped {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
authors: Vec<String>,
|
||||
version: Option<String>,
|
||||
icon: Option<PathBuf>,
|
||||
package_type: Option<String>,
|
||||
},
|
||||
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<String>,
|
||||
pub package_type: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub main: String,
|
||||
pub versions: Vec<String>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
pub id: String,
|
||||
pub uid: Option<String>,
|
||||
pub package_type: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub main: String,
|
||||
pub versions: Vec<String>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
pub downloads: u32,
|
||||
pub followers: u32,
|
||||
pub categories: Vec<String>,
|
||||
pub optional_categories: Option<Vec<String>>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub client: PackageSide,
|
||||
pub server: PackageSide,
|
||||
pub downloads: u32,
|
||||
pub followers: u32,
|
||||
pub categories: Vec<String>,
|
||||
pub optional_categories: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 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 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<String>,
|
||||
pub featured: bool,
|
||||
pub version_id: String,
|
||||
pub changelog: String,
|
||||
pub changelog_url: Option<String>,
|
||||
|
||||
pub published: DateTime<Utc>,
|
||||
pub downloads: u32,
|
||||
pub version_type: String,
|
||||
pub published: DateTime<Utc>,
|
||||
pub downloads: u32,
|
||||
pub version_type: String,
|
||||
|
||||
pub files: Vec<ManagedVersionFile>,
|
||||
pub deps: Vec<ManagedDependency>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub files: Vec<ManagedVersionFile>,
|
||||
pub deps: Vec<ManagedDependency>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
}
|
||||
|
||||
/// 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<PackageFile>,
|
||||
pub hashes: HashMap<String, String>,
|
||||
pub size: u32,
|
||||
pub file_type: Option<PackageFile>,
|
||||
pub hashes: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Universal interface for managed package dependencies.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ManagedDependency {
|
||||
pub version_id: Option<String>,
|
||||
pub package_id: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
pub dependency_type: PackageDependency,
|
||||
pub version_id: Option<String>,
|
||||
pub package_id: Option<String>,
|
||||
pub file_name: Option<String>,
|
||||
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<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub id: String,
|
||||
pub role: String,
|
||||
pub username: String,
|
||||
pub name: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
cache_path: &Path,
|
||||
path: &PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
icon_path: Option<String>,
|
||||
cache_path: &Path,
|
||||
path: &PathBuf,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<String>,
|
||||
/// Global and default custom environment variables.
|
||||
pub custom_env_args: Vec<(String, String)>,
|
||||
/// Global and default maximum [`Semaphore`] I/O operations.
|
||||
pub max_async_io_operations: usize,
|
||||
/// Global and default maximum [`Semaphore`] HTTP operations.
|
||||
pub max_async_fetches: usize,
|
||||
/// The [`CURRENT_FORMAT_VERSION`] of this settings file.
|
||||
pub version: u32,
|
||||
/// Whether or not to disable Discord IPC.
|
||||
#[serde(default)]
|
||||
/// 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<String>,
|
||||
/// Global and default custom environment variables.
|
||||
pub custom_env_args: Vec<(String, String)>,
|
||||
/// Global and default maximum [`Semaphore`] I/O operations.
|
||||
pub max_async_io_operations: usize,
|
||||
/// Global and default maximum [`Semaphore`] HTTP operations.
|
||||
pub max_async_fetches: usize,
|
||||
/// The [`CURRENT_FORMAT_VERSION`] of this settings file.
|
||||
pub version: u32,
|
||||
/// Whether or not to disable Discord IPC.
|
||||
#[serde(default)]
|
||||
pub disable_discord: bool,
|
||||
/// Whether or not to enable a debug mode in the launcher.
|
||||
#[serde(default)]
|
||||
pub debug_mode: bool,
|
||||
/// Whether or not to disable Plausible and Sentry analytics.
|
||||
#[serde(default)]
|
||||
pub disable_analytics: bool,
|
||||
/// The core global config directory stored as a [`PathBuf`].
|
||||
#[serde(default = "Directories::init_settings_dir")]
|
||||
pub config_dir: Option<PathBuf>,
|
||||
/// Whether or not to enable a debug mode in the launcher.
|
||||
#[serde(default)]
|
||||
pub debug_mode: bool,
|
||||
/// Whether or not to disable Plausible and Sentry analytics.
|
||||
#[serde(default)]
|
||||
pub disable_analytics: bool,
|
||||
/// The core global config directory stored as a [`PathBuf`].
|
||||
#[serde(default = "Directories::init_settings_dir")]
|
||||
pub config_dir: Option<PathBuf>,
|
||||
/// Whether or not to minimize the launcher upon a game launch.
|
||||
#[serde(default)]
|
||||
pub hide_on_launch: bool,
|
||||
/// Enable/disable 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<Self> {
|
||||
let mut recovered_corruption = false;
|
||||
/// Initializes the global settings state.
|
||||
#[tracing::instrument]
|
||||
pub async fn initialize(file: &Path) -> crate::Result<Self> {
|
||||
let mut recovered_corruption = false;
|
||||
|
||||
let settings = if file.exists() {
|
||||
let read_settings = io::read(&file)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
anyhow::anyhow!("error reading settings file: {0}", err).into()
|
||||
})
|
||||
.and_then(|it| {
|
||||
serde_json::from_slice::<Settings>(&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::<Settings>(&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<String>,
|
||||
/// Wrapper hook for the runtime of the game instance.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
/// Post-launch hook.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub post: Option<String>,
|
||||
/// Pre-launch hook.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pre: Option<String>,
|
||||
/// Wrapper hook for the runtime of the game instance.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub wrapper: Option<String>,
|
||||
/// Post-launch hook.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub post: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@
|
|||
//! 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<Bytes> {
|
||||
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<T>(
|
|||
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<T>(
|
||||
path: &Path,
|
||||
semaphore: &IoSemaphore,
|
||||
) -> crate::Result<T>
|
||||
pub async fn read_json<T>(path: &Path, semaphore: &IoSemaphore) -> crate::Result<T>
|
||||
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,15 +272,28 @@ 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<PathBuf> {
|
||||
pub async fn write_icon(
|
||||
icon_path: &str,
|
||||
cache_path: &Path,
|
||||
bytes: Bytes,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<PathBuf> {
|
||||
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?;
|
||||
|
||||
|
|
@ -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<String> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
//!
|
||||
//! 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<std::path::Path>,
|
||||
) -> Self {
|
||||
pub fn with_path(source: std::io::Error, path: impl AsRef<std::path::Path>) -> 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<std::path::Path>,
|
||||
) -> Result<std::path::PathBuf, IOError> {
|
||||
pub fn canonicalize(path: impl AsRef<std::path::Path>) -> Result<std::path::PathBuf, IOError> {
|
||||
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<std::path::Path>,
|
||||
) -> Result<tokio::fs::ReadDir, IOError> {
|
||||
pub async fn read_dir(path: impl AsRef<std::path::Path>) -> Result<tokio::fs::ReadDir, IOError> {
|
||||
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<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
pub async fn create_dir_all(path: impl AsRef<std::path::Path>) -> 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<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
pub async fn remove_dir_all(path: impl AsRef<std::path::Path>) -> 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<std::path::Path>,
|
||||
) -> Result<String, IOError> {
|
||||
pub async fn read_to_string(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
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<std::path::Path>,
|
||||
) -> Result<Vec<u8>, IOError> {
|
||||
pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, 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<Path>`].
|
||||
pub async fn write(
|
||||
path: impl AsRef<std::path::Path>,
|
||||
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<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
pub async fn remove_file(path: impl AsRef<std::path::Path>) -> 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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,19 @@
|
|||
//!
|
||||
//! 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<Vec<JavaVersion>, 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<PathBuf> {
|
||||
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<String, std::io::Error> = 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<String, std::io::Error> = 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<Vec<JavaVersion>, 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<Vec<JavaVersion>, 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<HashSet<PathBuf>, 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<PathBuf> {
|
||||
let paths = env::var("PATH").map(|p| env::split_paths(&p).collect::<HashSet<_>>());
|
||||
paths.unwrap_or_else(|_| HashSet::new())
|
||||
let paths = env::var("PATH").map(|p| env::split_paths(&p).collect::<HashSet<_>>());
|
||||
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<PathBuf>) -> HashSet<JavaVersion> {
|
||||
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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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<PathBuf>) -> HashSet<JavaVersion> {
|
|||
#[tracing::instrument]
|
||||
#[onelauncher_debug::debugger]
|
||||
pub async fn check_java_instance(path: &Path) -> Option<JavaVersion> {
|
||||
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::<u32>()?;
|
||||
minor.parse::<u32>()?
|
||||
} else {
|
||||
major = 1;
|
||||
major_opt.ok_or_else(|| JavaError::InvalidVersion(version.to_string()))?.parse::<u32>()?
|
||||
};
|
||||
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::<u32>()?;
|
||||
minor.parse::<u32>()?
|
||||
} else {
|
||||
major = 1;
|
||||
major_opt
|
||||
.ok_or_else(|| JavaError::InvalidVersion(version.to_string()))?
|
||||
.parse::<u32>()?
|
||||
};
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -2,73 +2,78 @@
|
|||
//!
|
||||
//! 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<Debouncer<RecommendedWatcher>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::Wry> {
|
||||
tauri::plugin::Builder::new("onelauncher")
|
||||
.setup(|app, _| {
|
||||
|
|
@ -13,14 +12,7 @@ pub fn init() -> TauriPlugin<tauri::Wry> {
|
|||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
|
|
@ -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: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue