fix: java downloading reading entire archive into memory twice
This commit is contained in:
parent
a4d07d3fa3
commit
bb84df1091
13 changed files with 104 additions and 812 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -369,6 +369,15 @@ version = "4.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-tempfile"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8a57b75c36e16f4d015e60e6a177552976a99b6947724403c551bcfa7cb1207"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.88"
|
||||
|
|
@ -3430,6 +3439,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-compression",
|
||||
"async-stream",
|
||||
"async-tempfile",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"base64 0.22.1",
|
||||
|
|
@ -3469,7 +3479,6 @@ dependencies = [
|
|||
"sysinfo",
|
||||
"tauri",
|
||||
"taurpc",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ dirs = { version = "=6.0.0" }
|
|||
dunce = { version = "=1.0.5" }
|
||||
flate2 = { version = "=1.1.1" }
|
||||
sanitize-filename = { version = "=0.6.0" }
|
||||
tempfile = { version = "=3.19.1" }
|
||||
async-tempfile = { version = "=0.7.0" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio-stream = { version = "0.1", features = [ "fs" ] }
|
||||
tokio-util = { version = "0.7", features = [ "compat" ] }
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ bytes = { workspace = true }
|
|||
base64 = { workspace = true }
|
||||
async-compression = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
async-tempfile = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
|
||||
# features[tauri]
|
||||
|
|
|
|||
26
packages/core/examples/download_java.rs
Normal file
26
packages/core/examples/download_java.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use onelauncher_core::api::java;
|
||||
use onelauncher_core::api::proxy::ProxyDynamic;
|
||||
use onelauncher_core::error::LauncherResult;
|
||||
use onelauncher_core::initialize_core;
|
||||
use onelauncher_core::store::{CoreOptions, State};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> LauncherResult<()> {
|
||||
initialize_core(CoreOptions::default(), ProxyDynamic::new()).await?;
|
||||
|
||||
let _ = State::get().await?;
|
||||
|
||||
let packages = java::get_zulu_packages().await?;
|
||||
|
||||
let Some(pkg) = packages.first() else {
|
||||
return Err(anyhow::anyhow!("no zulu package found").into());
|
||||
};
|
||||
|
||||
println!("found package: {pkg:#?}");
|
||||
|
||||
let path = java::install_java_package(pkg).await?;
|
||||
|
||||
println!("java package installed at '{path:?}'");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_zip::base::read::mem::ZipFileReader;
|
||||
use futures::TryStreamExt;
|
||||
use interpulse::api::minecraft::VersionInfo;
|
||||
use onelauncher_entity::{java_versions, setting_profiles};
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress};
|
||||
use crate::constants::JAVA_BIN;
|
||||
use crate::error::LauncherResult;
|
||||
use crate::store::Dirs;
|
||||
use crate::store::ingress::{IngressType, SubIngress};
|
||||
use crate::utils::http::{fetch_advanced, fetch_json};
|
||||
use crate::store::ingress::IngressType;
|
||||
use crate::utils::http;
|
||||
use crate::utils::io::{self, IOError};
|
||||
|
||||
pub mod dao;
|
||||
|
|
@ -44,15 +47,11 @@ pub fn get_java_bin() -> PathBuf {
|
|||
.join("Contents")
|
||||
.join("Home")
|
||||
.join("bin")
|
||||
.join("java")
|
||||
.join(JAVA_BIN)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
PathBuf::new().join("bin").join("javaw.exe")
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
{
|
||||
PathBuf::new().join("bin").join("java")
|
||||
PathBuf::new().join("bin").join(JAVA_BIN)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +64,7 @@ pub struct JavaInfo {
|
|||
}
|
||||
|
||||
#[onelauncher_macro::specta]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct JavaPackage {
|
||||
pub download_url: String,
|
||||
pub name: PathBuf,
|
||||
|
|
@ -88,15 +87,15 @@ pub async fn check_java_runtime(
|
|||
.await?;
|
||||
let id = id.as_ref();
|
||||
|
||||
let dir = io::tempdir()?;
|
||||
let file = dir.path().join("JavaInfo.class");
|
||||
let dir = io::tempdir().await?;
|
||||
let file = dir.dir_path().join("JavaInfo.class");
|
||||
id.map(async |id| send_ingress(id, 25.0).await);
|
||||
|
||||
io::write(&file, JAVA_INFO_CLASS).await?;
|
||||
id.map(async |id| send_ingress(id, 25.0).await);
|
||||
let java_info = tokio::process::Command::new(absolute_path)
|
||||
.arg("-cp")
|
||||
.arg(dir.path())
|
||||
.arg(dir.dir_path())
|
||||
.arg("JavaInfo")
|
||||
.env_remove("_JAVA_OPTIONS")
|
||||
.output()
|
||||
|
|
@ -207,7 +206,7 @@ pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
|
|||
fn find_java_in_path(mut found: Vec<PathBuf>) -> Vec<PathBuf> {
|
||||
if let Ok(path) = std::env::var("PATH") {
|
||||
for path_entry in path.split(':') {
|
||||
let java = PathBuf::from(path_entry).join("java");
|
||||
let java = PathBuf::from(path_entry).join(JAVA_BIN);
|
||||
if java.exists() && java.is_file() && !found.contains(&java) {
|
||||
found.push(java);
|
||||
}
|
||||
|
|
@ -264,7 +263,7 @@ fn internal_locate_java() -> Vec<PathBuf> {
|
|||
if let Ok(entries) = std::fs::read_dir(base_path) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
|
||||
let java_exe = entry.path().join("bin").join("java.exe");
|
||||
let java_exe = entry.path().join(get_java_bin());
|
||||
if java_exe.exists() {
|
||||
java_homes.push(java_exe);
|
||||
}
|
||||
|
|
@ -276,14 +275,14 @@ fn internal_locate_java() -> Vec<PathBuf> {
|
|||
// env vars
|
||||
if let Ok(java_home) = std::env::var("JAVA_HOME") {
|
||||
let path_buf = PathBuf::from(java_home);
|
||||
if path_buf.join("bin").join("java.exe").exists() {
|
||||
if path_buf.join("bin").join(JAVA_BIN).exists() {
|
||||
java_homes.push(path_buf);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path) = std::env::var("PATH") {
|
||||
for path_entry in path.split(';') {
|
||||
let java_exe = PathBuf::from(path_entry).join("java.exe");
|
||||
let java_exe = PathBuf::from(path_entry).join(JAVA_BIN);
|
||||
if java_exe.exists() {
|
||||
if let Some(bin_dir) = java_exe.parent() {
|
||||
if let Some(java_home) = bin_dir.parent() {
|
||||
|
|
@ -363,7 +362,7 @@ fn internal_locate_java() -> Vec<PathBuf> {
|
|||
}
|
||||
|
||||
pub async fn get_zulu_packages() -> LauncherResult<Vec<JavaPackage>> {
|
||||
fetch_json::<Vec<JavaPackage>>(
|
||||
http::fetch_json::<Vec<JavaPackage>>(
|
||||
reqwest::Method::GET,
|
||||
format!(
|
||||
"https://api.azul.com/metadata/v1/zulu/packages/?os={}&arch={}&archive_type=zip&java_package_type=jre&javafx_bundled=false&latest=true&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100",
|
||||
|
|
@ -384,16 +383,14 @@ pub async fn install_java_from_major(version: u32) -> LauncherResult<PathBuf> {
|
|||
.find(|p| p.java_version.contains(&version))
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not find a java package for version {}", version))?;
|
||||
|
||||
install_java_package(package).await
|
||||
install_java_package(&package).await
|
||||
}
|
||||
|
||||
pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBuf> {
|
||||
pub async fn install_java_package(package: &JavaPackage) -> LauncherResult<PathBuf> {
|
||||
const INGRESS_TOTAL: f64 = 100.0;
|
||||
const INGRESS_TASKS: f64 = 4.0;
|
||||
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
|
||||
|
||||
let version = package.download_url;
|
||||
|
||||
let ingress_id = init_ingress(
|
||||
IngressType::JavaPrepare,
|
||||
&format!(
|
||||
|
|
@ -404,37 +401,40 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
|
|||
)
|
||||
.await?;
|
||||
|
||||
let file = fetch_advanced(
|
||||
reqwest::Method::GET,
|
||||
&version,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// send the request
|
||||
let res = http::request(Method::GET, &package.download_url).await?;
|
||||
let java_dir = Dirs::get_java_dir().await?;
|
||||
let archive = ZipFileReader::new(file.to_vec())
|
||||
.await
|
||||
.map_err(IOError::AsyncZipError)?;
|
||||
|
||||
if let Some(entry) = archive.file().entries().first()
|
||||
&& let Some(dir) = entry
|
||||
.filename()
|
||||
.clone()
|
||||
.into_string()
|
||||
.unwrap()
|
||||
.split('/')
|
||||
.next()
|
||||
{
|
||||
let path = java_dir.join(dir);
|
||||
if path.exists() {
|
||||
io::remove_dir_all(&path).await?;
|
||||
// prepare to download
|
||||
let size = res.content_length().unwrap_or(0);
|
||||
let mut tmp_archive = io::tempfile().await?;
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut last_ingress_prog: f64 = 0.0;
|
||||
|
||||
// download to tmpfile
|
||||
let mut stream = res.bytes_stream();
|
||||
while let Some(bytes) = stream.try_next().await? {
|
||||
let byte_len = bytes.len() as u64;
|
||||
downloaded += byte_len;
|
||||
|
||||
tmp_archive.write_all(&bytes).await.map_err(IOError::from)?;
|
||||
|
||||
if size > 0 {
|
||||
let absolute = (downloaded as f64 / size as f64) * INGRESS_STEP;
|
||||
let delta = absolute - last_ingress_prog;
|
||||
send_ingress(&ingress_id, delta).await?;
|
||||
last_ingress_prog = absolute;
|
||||
}
|
||||
}
|
||||
|
||||
io::unzip_bytes(archive.data().to_vec(), &java_dir).await?;
|
||||
tmp_archive.flush().await.map_err(IOError::from)?;
|
||||
|
||||
// extract file
|
||||
io::extract_zip(tmp_archive.file_path(), &java_dir).await?;
|
||||
send_ingress(&ingress_id, INGRESS_STEP).await?;
|
||||
|
||||
// drop (which in turn closes / deletes the tempfile)
|
||||
drop(tmp_archive);
|
||||
|
||||
let mut base_path = java_dir.join(
|
||||
package
|
||||
|
|
@ -453,18 +453,10 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
|
|||
.map(|v| v.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
base_path = base_path
|
||||
.join(format!("zulu-{java_version}.jre"))
|
||||
.join("Contents")
|
||||
.join("Home")
|
||||
.join("bin")
|
||||
.join("java");
|
||||
base_path = base_path.join(format!("zulu-{java_version}.jre"));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
base_path = base_path.join("bin").join(crate::constants::JAVA_BIN);
|
||||
}
|
||||
base_path = base_path.join(get_java_bin());
|
||||
|
||||
Ok(base_path)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ pub struct Settings {
|
|||
pub max_concurrent_requests: usize,
|
||||
pub settings_version: u32,
|
||||
pub native_window_frame: bool,
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
pub show_tanstack_dev_tools: bool,
|
||||
}
|
||||
|
||||
|
|
@ -32,6 +34,8 @@ impl Default for Settings {
|
|||
max_concurrent_requests: 25,
|
||||
settings_version: 1,
|
||||
native_window_frame: false,
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
show_tanstack_dev_tools: tauri::is_dev(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ use std::sync::Arc;
|
|||
|
||||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use async_stream::try_stream;
|
||||
use async_tempfile::{TempDir, TempFile};
|
||||
use async_zip::StoredZipEntry;
|
||||
use async_zip::base::read::WithoutEntry;
|
||||
use futures::{Stream, TryStreamExt, pin_mut};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tempfile::TempDir;
|
||||
use tokio_util::compat::TokioAsyncWriteCompatExt;
|
||||
|
||||
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
|
||||
|
|
@ -46,6 +46,12 @@ pub enum IOError {
|
|||
#[skip]
|
||||
async_zip::error::ZipError,
|
||||
),
|
||||
#[error("tempfile error: {0}")]
|
||||
TempFileError(
|
||||
#[from]
|
||||
#[skip]
|
||||
async_tempfile::Error,
|
||||
),
|
||||
}
|
||||
|
||||
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
|
||||
|
|
@ -256,13 +262,13 @@ pub async fn remove_file(path: impl AsRef<std::path::Path>) -> Result<(), IOErro
|
|||
}
|
||||
|
||||
/// Creates a temporary directory.
|
||||
pub fn tempdir() -> Result<TempDir, IOError> {
|
||||
Ok(tempfile::tempdir()?)
|
||||
pub async fn tempdir() -> Result<TempDir, IOError> {
|
||||
Ok(TempDir::new().await?)
|
||||
}
|
||||
|
||||
/// Creates a temporary file.
|
||||
pub fn tempfile() -> Result<std::fs::File, IOError> {
|
||||
Ok(tempfile::tempfile()?)
|
||||
pub async fn tempfile() -> Result<TempFile, IOError> {
|
||||
Ok(TempFile::new().await?)
|
||||
}
|
||||
|
||||
/// Reads a zip archive from a byte array
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
[package]
|
||||
name = "onelauncher_utils"
|
||||
description = "rust utils for onelauncher"
|
||||
version = { workspace = true }
|
||||
license = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
readme = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
pin-project-lite = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
sys-info = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
async-compression = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
nom = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
interpulse = { workspace = true }
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "@onelauncher/utils",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"description": "Next-generation open source Minecraft launcher utils",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:polyfrost/onelauncher",
|
||||
"directory": "packages/utils"
|
||||
},
|
||||
"bugs": "https://github.com/polyfrost/onelauncher/issues",
|
||||
"sideEffects": false,
|
||||
"scripts": {}
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
//! **IO Utilities**
|
||||
//!
|
||||
//! Async wrapper around [`tokio::fs`] and [`std::io::Error`] for our error system.
|
||||
|
||||
use async_compression::tokio::bufread::GzipDecoder;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
pub enum IOError {
|
||||
/// A wrapped [`std::io::Error`] along with the path involved in the error.
|
||||
#[error("error acessing path: {source}, path: {path}")]
|
||||
IOErrorWrapper {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
path: String,
|
||||
},
|
||||
/// A wrapped [`zip::result::ZipError`].
|
||||
#[error(transparent)]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
/// A wrapped [`std::io::Error`].
|
||||
#[error(transparent)]
|
||||
IOError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
|
||||
fn from((path, source): (P, std::io::Error)) -> Self {
|
||||
Self::IOErrorWrapper {
|
||||
source,
|
||||
path: path.as_ref().to_string_lossy().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
#[error("received a non UTF-8 path: <lossy_path='{}'>", .0.to_string_lossy())]
|
||||
pub struct NonUtf8PathError(pub Box<std::path::Path>);
|
||||
|
||||
impl IOError {
|
||||
/// Converts a [`std::io::Error`] into an [`IOError`].
|
||||
#[must_use]
|
||||
pub const fn from(source: std::io::Error) -> Self {
|
||||
Self::IOError(source)
|
||||
}
|
||||
|
||||
/// Converts a [`zip::result::ZipError`] into an [`IOError`].
|
||||
#[must_use]
|
||||
pub const fn from_zip(source: zip::result::ZipError) -> Self {
|
||||
Self::ZipError(source)
|
||||
}
|
||||
|
||||
/// Converts a [`std::io::Error`] and the path involved in the error into an [`IOErrror`].
|
||||
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> {
|
||||
let path = path.as_ref();
|
||||
dunce::canonicalize(path).map_err(|e| IOError::IOErrorWrapper {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::read_dir(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOErrorWrapper {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a directory if they are missing.
|
||||
pub async fn create_dir(path: impl AsRef<std::path::Path>) -> Result<(), IOError> {
|
||||
let path = path.as_ref();
|
||||
if path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::fs::create_dir(path)
|
||||
.await
|
||||
.map_err(|e| IOError::IOErrorWrapper {
|
||||
source: e,
|
||||
path: path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a future which will open a gzip compressed file for reading and read the entire contents into a string and return said string.
|
||||
pub async fn read_gz_to_string(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
let mut f = tokio::fs::File::open(path).await?;
|
||||
let mut buf = vec![];
|
||||
f.read_to_end(&mut buf).await?;
|
||||
|
||||
let mut decoder = GzipDecoder::new(buf.as_slice());
|
||||
let mut dst = String::new();
|
||||
decoder.read_to_string(&mut dst).await?;
|
||||
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the entire contents of a file into a bytes vector.
|
||||
pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError> {
|
||||
let path = path.as_ref();
|
||||
tokio::fs::read(path)
|
||||
.await
|
||||
.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]>,
|
||||
) -> 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"))??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write to a tempfile that is then transferred to an official [`AsRef<Path>`].
|
||||
fn sync_write(
|
||||
data: impl AsRef<[u8]>,
|
||||
path: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut tempfile = NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"failed to get parent directory of a tempfile",
|
||||
)
|
||||
})?)?;
|
||||
tempfile.write_all(data.as_ref())?;
|
||||
let tmp_path = tempfile.into_temp_path();
|
||||
let path = path.as_ref();
|
||||
// this is a sorta dangerous call but shouldnt matter because of async
|
||||
tmp_path.persist(path)?;
|
||||
std::io::Result::Ok(())
|
||||
}
|
||||
|
||||
/// Renames a file or directory to a new name, replacing the original file if `to` already exists.
|
||||
pub async fn rename(
|
||||
from: impl AsRef<std::path::Path>,
|
||||
to: impl AsRef<std::path::Path>,
|
||||
) -> Result<(), IOError> {
|
||||
let from = from.as_ref();
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn copy(
|
||||
from: impl AsRef<std::path::Path>,
|
||||
to: impl AsRef<std::path::Path>,
|
||||
) -> Result<u64, IOError> {
|
||||
let from = from.as_ref();
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes a file from the filesystem.
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
///// Returns the SHA512 hash of a file.
|
||||
// pub fn sha512(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
|
||||
// let path = path.as_ref();
|
||||
// let mut file = std::fs::File::open(path)?;
|
||||
// let mut hasher = sha2::Sha512::new();
|
||||
// std::io::copy(&mut file, &mut hasher)?;
|
||||
|
||||
// Ok(format!("{:x}", hasher.finalize()))
|
||||
// }
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
//! **`OneLauncher` Utilities**
|
||||
//! Standard utilities for use within all Rust subprojects.
|
||||
//!
|
||||
//! - [`logging`]: Async utilities for log4j parsing with [`nom`].
|
||||
//! - [`platform`]: Async utilities for managing OS-specific [`interpulse`] operations and rules.
|
||||
//! - [`io`]: Async wrapper around [`tokio::fs`] and [`std::io::Error`] for our error system.
|
||||
|
||||
pub mod io;
|
||||
pub mod logging;
|
||||
pub mod platform;
|
||||
|
||||
/// Simple macro that takes a mutable reference and inserts it into a codeblock closure
|
||||
/// as an owned reference.
|
||||
///
|
||||
/// mutable reference gets epically owned by free thinking macro!!!!! (not clickbait)
|
||||
/// im going insane insane insane insane insane insane insane insane insane
|
||||
#[macro_export]
|
||||
macro_rules! ref_owned {
|
||||
($id:ident = $init:expr => $transform:block) => {{
|
||||
let mut it = $init;
|
||||
{
|
||||
let $id = &mut it;
|
||||
$transform;
|
||||
}
|
||||
it
|
||||
}};
|
||||
}
|
||||
|
||||
/// Combines an iterator of `T` and an iterator of [`Option<T>`],
|
||||
/// removing and ensuring the safety of any [`None`] values in the process.
|
||||
pub fn chain_iterator<T>(
|
||||
required: impl IntoIterator<Item = T>,
|
||||
optional: impl IntoIterator<Item = Option<T>>,
|
||||
) -> Vec<T> {
|
||||
required
|
||||
.into_iter()
|
||||
.map(Some)
|
||||
.chain(optional)
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn test_type_of<T>(_: T) -> &'static str {
|
||||
std::any::type_name::<T>()
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
//! Async utilities for parsing Log4j entries with [`nom`].
|
||||
//! From <https://github.com/gorilla-devs/GDLauncher-Carbon/blob/develop/crates/carbon_parsing/src/log.rs>
|
||||
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::streaming::{tag, take_until};
|
||||
use nom::character::streaming::{char, multispace0, u64};
|
||||
use nom::combinator::{map, value};
|
||||
use nom::error::ParseError;
|
||||
use nom::multi::count;
|
||||
use nom::sequence::{delimited, preceded, separated_pair, tuple};
|
||||
use nom::IResult;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct LogEntry<'a> {
|
||||
pub logger: &'a str,
|
||||
pub level: LogLevel,
|
||||
pub timestamp: u64,
|
||||
pub thread_name: &'a str,
|
||||
pub message: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
fn whitespace<'a, O, E: ParseError<&'a str>>(
|
||||
inner: impl FnMut(&'a str) -> IResult<&'a str, O, E>,
|
||||
) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> {
|
||||
delimited(multispace0, inner, multispace0)
|
||||
}
|
||||
|
||||
pub fn parse_logentry(input: &str) -> IResult<&str, LogEntry<'_>> {
|
||||
let (o, (attributes, _, message)) = preceded(
|
||||
multispace0,
|
||||
alt((
|
||||
delimited(
|
||||
tag("<log4j:Event"),
|
||||
tuple((attributes, tag(">"), whitespace(message))),
|
||||
tag("</log4j:Event>"),
|
||||
),
|
||||
plain_text,
|
||||
)),
|
||||
)(input)?;
|
||||
|
||||
let Attributes {
|
||||
logger,
|
||||
level,
|
||||
timestamp,
|
||||
thread_name,
|
||||
} = attributes;
|
||||
|
||||
Ok((
|
||||
o,
|
||||
LogEntry {
|
||||
logger,
|
||||
level,
|
||||
timestamp,
|
||||
thread_name,
|
||||
message,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Attributes<'a> {
|
||||
pub logger: &'a str,
|
||||
pub level: LogLevel,
|
||||
pub timestamp: u64,
|
||||
pub thread_name: &'a str,
|
||||
}
|
||||
|
||||
fn attributes(input: &str) -> IResult<&str, Attributes<'_>> {
|
||||
let (o, attributes) = count(whitespace(attribute), 4)(input)?;
|
||||
macro_rules! extract_attribute {
|
||||
($field:ident) => {{
|
||||
let err = nom::Err::Error(nom::error::Error::from_error_kind(
|
||||
o,
|
||||
nom::error::ErrorKind::Alt,
|
||||
));
|
||||
let Attribute::$field(field) = attributes
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|a| matches!(a, Attribute::$field(_)))
|
||||
.ok_or(err)?
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
field
|
||||
}};
|
||||
}
|
||||
|
||||
Ok((
|
||||
o,
|
||||
Attributes {
|
||||
logger: extract_attribute!(Logger),
|
||||
level: extract_attribute!(Level),
|
||||
timestamp: extract_attribute!(Timestamp),
|
||||
thread_name: extract_attribute!(ThreadName),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Attribute<'a> {
|
||||
Logger(&'a str),
|
||||
Level(LogLevel),
|
||||
Timestamp(u64),
|
||||
ThreadName(&'a str),
|
||||
}
|
||||
|
||||
fn attribute(input: &str) -> IResult<&str, Attribute<'_>> {
|
||||
alt((attr_logger, attr_timestamp, attr_level, attr_thread))(input)
|
||||
}
|
||||
|
||||
fn attr_logger(input: &str) -> IResult<&str, Attribute<'_>> {
|
||||
map(
|
||||
separated_pair(
|
||||
tag("logger"),
|
||||
whitespace(char('=')),
|
||||
map(quoted_string, Attribute::Logger),
|
||||
),
|
||||
|(_, attr)| attr,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn attr_timestamp(input: &str) -> IResult<&str, Attribute<'_>> {
|
||||
map(
|
||||
separated_pair(
|
||||
tag("timestamp"),
|
||||
whitespace(char('=')),
|
||||
delimited(char('"'), map(u64, Attribute::Timestamp), char('"')),
|
||||
),
|
||||
|(_, attr)| attr,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn attr_level(input: &str) -> IResult<&str, Attribute<'_>> {
|
||||
map(
|
||||
separated_pair(
|
||||
tag("level"),
|
||||
whitespace(char('=')),
|
||||
delimited(char('"'), map(level, Attribute::Level), char('"')),
|
||||
),
|
||||
|(_, attr)| attr,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn attr_thread(input: &str) -> IResult<&str, Attribute<'_>> {
|
||||
map(
|
||||
separated_pair(
|
||||
tag("thread"),
|
||||
whitespace(char('=')),
|
||||
map(quoted_string, Attribute::ThreadName),
|
||||
),
|
||||
|(_, attr)| attr,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn quoted_string(input: &str) -> IResult<&str, &str> {
|
||||
delimited(char('"'), take_until("\""), char('"'))(input)
|
||||
}
|
||||
|
||||
fn level(input: &str) -> IResult<&str, LogLevel> {
|
||||
alt((
|
||||
value(LogLevel::Trace, tag("TRACE")),
|
||||
value(LogLevel::Debug, tag("DEBUG")),
|
||||
value(LogLevel::Info, tag("INFO")),
|
||||
value(LogLevel::Warn, tag("WARN")),
|
||||
value(LogLevel::Error, tag("ERROR")),
|
||||
))(input)
|
||||
}
|
||||
|
||||
/// Parses the message of the event.
|
||||
fn message(input: &str) -> IResult<&str, &str> {
|
||||
delimited(
|
||||
tag("<log4j:Message>"),
|
||||
whitespace(delimited(tag("<![CDATA["), take_until("]]>"), tag("]]>"))),
|
||||
tag("</log4j:Message>"),
|
||||
)(input)
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
fn plain_text(input: &str) -> IResult<&str, (Attributes<'_>, &str, &str)> {
|
||||
map(take_until("<"), |text| {
|
||||
(
|
||||
Attributes {
|
||||
logger: "OneLauncher",
|
||||
level: LogLevel::Info,
|
||||
timestamp: chrono::Local::now().timestamp_millis() as u64,
|
||||
thread_name: "N/A",
|
||||
},
|
||||
"",
|
||||
text,
|
||||
)
|
||||
})(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_message() {
|
||||
message(
|
||||
r"<log4j:Message>
|
||||
<![CDATA[200 Datafixer optimizations took 2000 years]]>
|
||||
</log4j:Message>",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_attributes() {
|
||||
let (_, attributes) = attributes(
|
||||
r#"
|
||||
logger="com.mojang.datafixers.DataFixerBuilder"
|
||||
timestamp="3213819318239"
|
||||
level="DEBUG"
|
||||
thread="Datafixer"
|
||||
>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
attributes,
|
||||
Attributes {
|
||||
logger: "com.mojang.datafixers.DataFixerBuilder",
|
||||
level: LogLevel::Debug,
|
||||
timestamp: 3_213_819_318_239,
|
||||
thread_name: "Datafixer"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_logger_attribute() {
|
||||
let (_, attr) = attr_logger(r#"logger="com.mojang.datafixers.DataFixerBuilder""#).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
attr,
|
||||
Attribute::Logger("com.mojang.datafixers.DataFixerBuilder")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_level_attribute() {
|
||||
let (_, attr) = attr_level(r#"level="INFO""#).unwrap();
|
||||
assert_eq!(attr, Attribute::Level(LogLevel::Info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_timestamp_attribute() {
|
||||
let (_, attr) = attr_timestamp(r#"timestamp="313213473127""#).unwrap();
|
||||
assert_eq!(attr, Attribute::Timestamp(313_213_473_127));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_thread_attribute() {
|
||||
let (_, attr) = attr_thread(r#"thread="Datafixer""#).unwrap();
|
||||
assert_eq!(attr, Attribute::ThreadName("Datafixer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_quoted_string() {
|
||||
let (_, res) = quoted_string(r#""girlskissing""#).unwrap();
|
||||
|
||||
assert_eq!(res, "girlskissing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_entry() {
|
||||
let (_, entry) = parse_logentry(
|
||||
r#"
|
||||
<log4j:Event
|
||||
logger="com.mojang.datafixers.DataFixerBuilder"
|
||||
timestamp="3213712731731"
|
||||
level="DEBUG"
|
||||
thread="Datafixer"
|
||||
>
|
||||
<log4j:Message>
|
||||
<![CDATA[200 Datafixer optimizations took 2000 years]]>
|
||||
</log4j:Message>
|
||||
</log4j:Event>
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
entry,
|
||||
LogEntry {
|
||||
logger: "com.mojang.datafixers.DataFixerBuilder",
|
||||
level: LogLevel::Debug,
|
||||
timestamp: 3_213_712_731_731,
|
||||
thread_name: "Datafixer",
|
||||
message: "200 Datafixer optimizations took 2000 years",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn parse_sample_log_entries() {
|
||||
// let mut input = include_str!("../../fixtures/test_log.xml");
|
||||
|
||||
// while let Ok((o, _)) = parse_logentry(input) {
|
||||
// input = o;
|
||||
// }
|
||||
|
||||
// assert_eq!(input, "exit code: 0");
|
||||
// }
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
//! **Platform Utilities**
|
||||
//!
|
||||
//! Async utilities for managing OS-specific [`interpulse`] operations and rules.
|
||||
|
||||
use interpulse::api::minecraft::{Os, OsRule};
|
||||
use regex::Regex;
|
||||
|
||||
/// An extension of [`Os`] for utilities on converting the current system's operating system
|
||||
/// into an [`Os`] structure for simple management.
|
||||
pub trait OsExt {
|
||||
/// Get the [`Os`] of the current system
|
||||
fn native() -> Self;
|
||||
|
||||
/// Get the [`Os`] of the current system along with it's arch from the `java_arch`.
|
||||
fn native_arch(java_arch: &str) -> Self;
|
||||
}
|
||||
|
||||
impl OsExt for Os {
|
||||
fn native_arch(java_arch: &str) -> Self {
|
||||
if std::env::consts::OS == "windows" {
|
||||
if java_arch == "aarch64" {
|
||||
Self::WindowsArm64
|
||||
} else {
|
||||
Self::Windows
|
||||
}
|
||||
} else if std::env::consts::OS == "linux" {
|
||||
if java_arch == "aarch64" {
|
||||
Self::LinuxArm64
|
||||
} else if java_arch == "arm" {
|
||||
Self::LinuxArm32
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
} else if std::env::consts::OS == "macos" {
|
||||
if java_arch == "aarch64" {
|
||||
Self::OsxArm64
|
||||
} else {
|
||||
Self::Osx
|
||||
}
|
||||
} else {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn native() -> Self {
|
||||
match std::env::consts::OS {
|
||||
"windows" => Self::Windows,
|
||||
"macos" => Self::Osx,
|
||||
"linux" => Self::Linux,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an os-specific [`OsRule`], returning if the [`Os`] matches the rule.
|
||||
#[must_use]
|
||||
pub fn os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
|
||||
let mut rule_match = true;
|
||||
|
||||
if let Some(ref arch) = rule.arch {
|
||||
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
|
||||
}
|
||||
|
||||
if let Some(name) = &rule.name {
|
||||
if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) {
|
||||
rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name;
|
||||
} else {
|
||||
rule_match &= &Os::native_arch(java_arch) == name;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &rule.version {
|
||||
if let Ok(regex) = Regex::new(version.as_str()) {
|
||||
rule_match &= regex.is_match(&sys_info::os_release().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
rule_match
|
||||
}
|
||||
|
||||
/// Gets the seperator between Java classpaths in metadata (`";"` on Windows, `";"` on others).
|
||||
#[must_use]
|
||||
pub fn classpath_separator(java_arch: &str) -> &'static str {
|
||||
match Os::native_arch(java_arch) {
|
||||
Os::Osx | Os::OsxArm64 | Os::Linux | Os::LinuxArm32 | Os::LinuxArm64 | Os::Unknown => ":",
|
||||
Os::Windows | Os::WindowsArm64 => ";",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue