Merge commit '7fa442fb28a2b9156690ff147206275163e7aec8' into beta
|
Before Width: | Height: | Size: 7.0 KiB |
BIN
packages/app-lib/src/api/minecraft_skins/assets/test/legacy.png
Normal file
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 1.8 KiB |
BIN
packages/app-lib/src/api/minecraft_skins/assets/test/notch.png
Normal file
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,12 +1,14 @@
|
||||
//! Miscellaneous PNG utilities for Minecraft skins.
|
||||
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
|
||||
use base64::Engine;
|
||||
use bytemuck::{AnyBitPattern, NoUninit};
|
||||
use bytes::Bytes;
|
||||
use data_url::DataUrl;
|
||||
use futures::{Stream, TryStreamExt, future::Either, stream};
|
||||
use itertools::Itertools;
|
||||
use rgb::Rgba;
|
||||
use tokio_util::{compat::FuturesAsyncReadCompatExt, io::SyncIoBridge};
|
||||
use url::Url;
|
||||
|
||||
@@ -84,10 +86,10 @@ pub fn dimensions(png_data: &[u8]) -> crate::Result<(u32, u32)> {
|
||||
Ok((width, height))
|
||||
}
|
||||
|
||||
/// Normalizes the texture of a Minecraft skin to the modern 64x64 format, handling
|
||||
/// legacy 64x32 skins as the vanilla game client does. This function prioritizes
|
||||
/// PNG encoding speed over compression density, so the resulting textures are better
|
||||
/// suited for display purposes, not persistent storage or transmission.
|
||||
/// Normalizes the texture of a Minecraft skin to the modern 64x64 format, handling legacy 64x32
|
||||
/// skins, doing "Notch transparency hack" and making inner parts opaque as the vanilla game client
|
||||
/// does. This function prioritizes PNG encoding speed over compression density, so the resulting
|
||||
/// textures are better suited for display purposes, not persistent storage or transmission.
|
||||
///
|
||||
/// The normalized, processed is returned texture as a byte array in PNG format.
|
||||
pub async fn normalize_skin_texture(
|
||||
@@ -131,43 +133,30 @@ pub async fn normalize_skin_texture(
|
||||
}
|
||||
|
||||
let is_legacy_skin = png_reader.info().height == 32;
|
||||
|
||||
let mut texture_buf = if is_legacy_skin {
|
||||
// Legacy skins have half the height, so duplicate the rows to
|
||||
// turn them into a 64x64 texture
|
||||
vec![0; png_reader.output_buffer_size() * 2]
|
||||
} else {
|
||||
// Modern skins are left as-is
|
||||
vec![0; png_reader.output_buffer_size()]
|
||||
};
|
||||
|
||||
let texture_buf_color_type = png_reader.output_color_type().0;
|
||||
png_reader.next_frame(&mut texture_buf)?;
|
||||
|
||||
let mut texture_buf =
|
||||
get_skin_texture_buffer(&mut png_reader, is_legacy_skin)?;
|
||||
if is_legacy_skin {
|
||||
convert_legacy_skin_texture(
|
||||
&mut texture_buf,
|
||||
texture_buf_color_type,
|
||||
png_reader.info(),
|
||||
)?;
|
||||
convert_legacy_skin_texture(&mut texture_buf, png_reader.info());
|
||||
do_notch_transparency_hack(&mut texture_buf, png_reader.info());
|
||||
}
|
||||
make_inner_parts_opaque(&mut texture_buf, png_reader.info());
|
||||
|
||||
let mut encoded_png = vec![];
|
||||
|
||||
let mut png_encoder = png::Encoder::new(&mut encoded_png, 64, 64);
|
||||
png_encoder.set_color(texture_buf_color_type);
|
||||
png_encoder.set_color(png::ColorType::Rgba);
|
||||
png_encoder.set_depth(png::BitDepth::Eight);
|
||||
png_encoder.set_filter(png::FilterType::NoFilter);
|
||||
png_encoder.set_compression(png::Compression::Fast);
|
||||
|
||||
// Keeping color space information properly set, to handle the occasional
|
||||
// strange PNG with non-sRGB chromacities and/or different grayscale spaces
|
||||
// strange PNG with non-sRGB chromaticities and/or different grayscale spaces
|
||||
// that keeps most people wondering, is what sets a carefully crafted image
|
||||
// manipulation routine apart :)
|
||||
if let Some(source_chromacities) =
|
||||
if let Some(source_chromaticities) =
|
||||
png_reader.info().source_chromaticities.as_ref().copied()
|
||||
{
|
||||
png_encoder.set_source_chromaticities(source_chromacities);
|
||||
png_encoder.set_source_chromaticities(source_chromaticities);
|
||||
}
|
||||
if let Some(source_gamma) =
|
||||
png_reader.info().source_gamma.as_ref().copied()
|
||||
@@ -178,8 +167,10 @@ pub async fn normalize_skin_texture(
|
||||
png_encoder.set_source_srgb(source_srgb);
|
||||
}
|
||||
|
||||
let png_buf = bytemuck::try_cast_slice(&texture_buf)
|
||||
.map_err(|_| ErrorKind::InvalidPng)?;
|
||||
let mut png_writer = png_encoder.write_header()?;
|
||||
png_writer.write_image_data(&texture_buf)?;
|
||||
png_writer.write_image_data(png_buf)?;
|
||||
png_writer.finish()?;
|
||||
|
||||
Ok(encoded_png.into())
|
||||
@@ -187,16 +178,71 @@ pub async fn normalize_skin_texture(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Reads a skin texture and returns a 64x64 buffer in RGBA format.
|
||||
fn get_skin_texture_buffer<R: Read>(
|
||||
png_reader: &mut png::Reader<R>,
|
||||
is_legacy_skin: bool,
|
||||
) -> crate::Result<Vec<Rgba<u8>>> {
|
||||
let mut png_buf = if is_legacy_skin {
|
||||
// Legacy skins have half the height, so duplicate the rows to
|
||||
// turn them into a 64x64 texture
|
||||
vec![0; png_reader.output_buffer_size() * 2]
|
||||
} else {
|
||||
// Modern skins are left as-is
|
||||
vec![0; png_reader.output_buffer_size()]
|
||||
};
|
||||
png_reader.next_frame(&mut png_buf)?;
|
||||
|
||||
let mut texture_buf = match png_reader.output_color_type().0 {
|
||||
png::ColorType::Grayscale => png_buf
|
||||
.iter()
|
||||
.map(|&value| Rgba {
|
||||
r: value,
|
||||
g: value,
|
||||
b: value,
|
||||
a: 255,
|
||||
})
|
||||
.collect_vec(),
|
||||
png::ColorType::GrayscaleAlpha => png_buf
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| Rgba {
|
||||
r: chunk[0],
|
||||
g: chunk[0],
|
||||
b: chunk[0],
|
||||
a: chunk[1],
|
||||
})
|
||||
.collect_vec(),
|
||||
png::ColorType::Rgb => png_buf
|
||||
.chunks_exact(3)
|
||||
.map(|chunk| Rgba {
|
||||
r: chunk[0],
|
||||
g: chunk[1],
|
||||
b: chunk[2],
|
||||
a: 255,
|
||||
})
|
||||
.collect_vec(),
|
||||
png::ColorType::Rgba => bytemuck::try_cast_vec(png_buf)
|
||||
.map_err(|_| ErrorKind::InvalidPng)?,
|
||||
_ => Err(ErrorKind::InvalidPng)?, // Cannot happen by PNG spec after transformations
|
||||
};
|
||||
|
||||
// Make the added bottom half of the expanded legacy skin buffer transparent
|
||||
if is_legacy_skin {
|
||||
set_alpha(&mut texture_buf, png_reader.info(), 0, 32, 64, 64, 0);
|
||||
}
|
||||
|
||||
Ok(texture_buf)
|
||||
}
|
||||
|
||||
/// Converts a legacy skin texture (32x64 pixels) within a 64x64 buffer to the
|
||||
/// native 64x64 format used by modern Minecraft clients.
|
||||
///
|
||||
/// See also 25w16a's `SkinTextureDownloader#processLegacySkin` method.
|
||||
#[inline]
|
||||
fn convert_legacy_skin_texture(
|
||||
texture_buf: &mut [u8],
|
||||
texture_color_type: png::ColorType,
|
||||
texture_buf: &mut [Rgba<u8, u8>],
|
||||
texture_info: &png::Info,
|
||||
) -> crate::Result<()> {
|
||||
) {
|
||||
/// The skin faces the game client copies around, in order, when converting a
|
||||
/// legacy skin to the native 64x64 format.
|
||||
const FACE_COPY_PARAMETERS: &[(
|
||||
@@ -222,33 +268,55 @@ fn convert_legacy_skin_texture(
|
||||
];
|
||||
|
||||
for (x, y, off_x, off_y, width, height) in FACE_COPY_PARAMETERS {
|
||||
macro_rules! do_copy {
|
||||
($pixel_type:ty) => {
|
||||
copy_rect_mirror_horizontally::<$pixel_type>(
|
||||
// This cast should never fail because all pixels have a depth of 8 bits
|
||||
// after the transformations applied during decoding
|
||||
::bytemuck::try_cast_slice_mut(texture_buf).map_err(|_| ErrorKind::InvalidPng)?,
|
||||
&texture_info,
|
||||
*x,
|
||||
*y,
|
||||
*off_x,
|
||||
*off_y,
|
||||
*width,
|
||||
*height,
|
||||
)
|
||||
};
|
||||
}
|
||||
copy_rect_mirror_horizontally(
|
||||
texture_buf,
|
||||
texture_info,
|
||||
*x,
|
||||
*y,
|
||||
*off_x,
|
||||
*off_y,
|
||||
*width,
|
||||
*height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
match texture_color_type.samples() {
|
||||
1 => do_copy!(rgb::Gray<u8>),
|
||||
2 => do_copy!(rgb::GrayAlpha<u8>),
|
||||
3 => do_copy!(rgb::Rgb<u8>),
|
||||
4 => do_copy!(rgb::Rgba<u8>),
|
||||
_ => Err(ErrorKind::InvalidPng)?, // Cannot happen by PNG spec after transformations
|
||||
};
|
||||
/// Makes outer head layer transparent if every pixel has alpha greater or equal to 128.
|
||||
///
|
||||
/// See also 25w16a's `SkinTextureDownloader#doNotchTransparencyHack` method.
|
||||
fn do_notch_transparency_hack(
|
||||
texture_buf: &mut [Rgba<u8, u8>],
|
||||
texture_info: &png::Info,
|
||||
) {
|
||||
// The skin part the game client makes transparent
|
||||
let (x1, y1, x2, y2) = (32, 0, 64, 32);
|
||||
|
||||
for y in y1..y2 {
|
||||
for x in x1..x2 {
|
||||
if texture_buf[x + y * texture_info.width as usize].a < 128 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
set_alpha(texture_buf, texture_info, x1, y1, x2, y2, 0);
|
||||
}
|
||||
|
||||
/// Makes inner parts of a skin texture opaque.
|
||||
///
|
||||
/// See also 25w16a's `SkinTextureDownloader#processLegacySkin` method.
|
||||
#[inline]
|
||||
fn make_inner_parts_opaque(
|
||||
texture_buf: &mut [Rgba<u8, u8>],
|
||||
texture_info: &png::Info,
|
||||
) {
|
||||
/// The skin parts the game client makes opaque.
|
||||
const OPAQUE_PART_PARAMETERS: &[(usize, usize, usize, usize)] =
|
||||
&[(0, 0, 32, 16), (0, 16, 64, 32), (16, 48, 48, 64)];
|
||||
|
||||
for (x1, y1, x2, y2) in OPAQUE_PART_PARAMETERS {
|
||||
set_alpha(texture_buf, texture_info, *x1, *y1, *x2, *y2, 255);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies a `width` pixels wide, `height` pixels tall rectangle of pixels within `texture_buf`
|
||||
@@ -260,8 +328,8 @@ fn convert_legacy_skin_texture(
|
||||
/// boolean, boolean)` method, but with the last two parameters fixed to `true` and `false`,
|
||||
/// respectively.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn copy_rect_mirror_horizontally<PixelType: NoUninit + AnyBitPattern>(
|
||||
texture_buf: &mut [PixelType],
|
||||
fn copy_rect_mirror_horizontally(
|
||||
texture_buf: &mut [Rgba<u8, u8>],
|
||||
texture_info: &png::Info,
|
||||
x: usize,
|
||||
y: usize,
|
||||
@@ -283,18 +351,27 @@ fn copy_rect_mirror_horizontally<PixelType: NoUninit + AnyBitPattern>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets alpha for every pixel of a rectangle within `texture_buf`
|
||||
/// whose top-left corner is at `(x1, y1)` and bottom-right corner is at `(x2 - 1, y2 - 1)`.
|
||||
fn set_alpha(
|
||||
texture_buf: &mut [Rgba<u8, u8>],
|
||||
texture_info: &png::Info,
|
||||
x1: usize,
|
||||
y1: usize,
|
||||
x2: usize,
|
||||
y2: usize,
|
||||
alpha: u8,
|
||||
) {
|
||||
for y in y1..y2 {
|
||||
for x in x1..x2 {
|
||||
texture_buf[x + y * texture_info.width as usize].a = alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[tokio::test]
|
||||
async fn normalize_skin_texture_works() {
|
||||
let legacy_png_data = &include_bytes!("assets/default/MissingNo.png")[..];
|
||||
let expected_normalized_png_data =
|
||||
&include_bytes!("assets/test/MissingNo_normalized.png")[..];
|
||||
|
||||
let normalized_png_data =
|
||||
normalize_skin_texture(&UrlOrBlob::Blob(legacy_png_data.into()))
|
||||
.await
|
||||
.expect("Failed to normalize skin texture");
|
||||
|
||||
let decode_to_pixels = |png_data: &[u8]| {
|
||||
let decoder = png::Decoder::new(png_data);
|
||||
let mut reader = decoder.read_info().expect("Failed to read PNG info");
|
||||
@@ -305,19 +382,55 @@ async fn normalize_skin_texture_works() {
|
||||
(buffer, reader.info().clone())
|
||||
};
|
||||
|
||||
let (normalized_pixels, normalized_info) =
|
||||
decode_to_pixels(&normalized_png_data);
|
||||
let (expected_pixels, expected_info) =
|
||||
decode_to_pixels(expected_normalized_png_data);
|
||||
let test_data = [
|
||||
(
|
||||
"legacy",
|
||||
&include_bytes!("assets/test/legacy.png")[..],
|
||||
&include_bytes!("assets/test/legacy_normalized.png")[..],
|
||||
),
|
||||
(
|
||||
"notch",
|
||||
&include_bytes!("assets/test/notch.png")[..],
|
||||
&include_bytes!("assets/test/notch_normalized.png")[..],
|
||||
),
|
||||
(
|
||||
"transparent",
|
||||
&include_bytes!("assets/test/transparent.png")[..],
|
||||
&include_bytes!("assets/test/transparent_normalized.png")[..],
|
||||
),
|
||||
];
|
||||
|
||||
// Check that dimensions match
|
||||
assert_eq!(normalized_info.width, expected_info.width);
|
||||
assert_eq!(normalized_info.height, expected_info.height);
|
||||
assert_eq!(normalized_info.color_type, expected_info.color_type);
|
||||
for (skin_name, original_png_data, expected_normalized_png_data) in
|
||||
test_data
|
||||
{
|
||||
let normalized_png_data =
|
||||
normalize_skin_texture(&UrlOrBlob::Blob(original_png_data.into()))
|
||||
.await
|
||||
.expect("Failed to normalize skin texture");
|
||||
|
||||
// Check that pixel data matches
|
||||
assert_eq!(
|
||||
normalized_pixels, expected_pixels,
|
||||
"Pixel data doesn't match"
|
||||
);
|
||||
let (normalized_pixels, normalized_info) =
|
||||
decode_to_pixels(&normalized_png_data);
|
||||
let (expected_pixels, expected_info) =
|
||||
decode_to_pixels(expected_normalized_png_data);
|
||||
|
||||
// Check that dimensions match
|
||||
assert_eq!(
|
||||
normalized_info.width, expected_info.width,
|
||||
"Widths don't match for {skin_name}"
|
||||
);
|
||||
assert_eq!(
|
||||
normalized_info.height, expected_info.height,
|
||||
"Heights don't match for {skin_name}"
|
||||
);
|
||||
assert_eq!(
|
||||
normalized_info.color_type, expected_info.color_type,
|
||||
"Color types don't match for {skin_name}"
|
||||
);
|
||||
|
||||
// Check that pixel data matches
|
||||
assert_eq!(
|
||||
normalized_pixels, expected_pixels,
|
||||
"Pixel data doesn't match for {skin_name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ pub mod prelude {
|
||||
jre, metadata, minecraft_auth, mr_auth, pack, process,
|
||||
profile::{self, Profile, create},
|
||||
settings,
|
||||
util::io::{IOError, canonicalize},
|
||||
util::{
|
||||
io::{IOError, canonicalize},
|
||||
network::{is_network_metered, tcp_listen_any_loopback},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::state::{CachedEntry, LinkedData, ProfileInstallStage, SideType};
|
||||
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
|
||||
use crate::util::io;
|
||||
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -27,7 +28,7 @@ pub struct PackFormat {
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PackFile {
|
||||
pub path: String,
|
||||
pub path: SafeRelativeUtf8UnixPathBuf,
|
||||
pub hashes: HashMap<PackFileHash, String>,
|
||||
pub env: Option<HashMap<EnvType, SideType>>,
|
||||
pub downloads: Vec<String>,
|
||||
|
||||
@@ -18,8 +18,8 @@ use super::install_from::{
|
||||
generate_pack_from_version_id,
|
||||
};
|
||||
use crate::data::ProjectType;
|
||||
use std::io::Cursor;
|
||||
use std::path::{Component, PathBuf};
|
||||
use std::io::{Cursor, ErrorKind};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Install a pack
|
||||
/// Wrapper around install_pack_files that generates a pack creation description, and
|
||||
@@ -169,31 +169,22 @@ pub async fn install_zipped_mrpack_files(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_path = project.path.to_string();
|
||||
let path = profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(project.path.as_str());
|
||||
|
||||
let path =
|
||||
std::path::Path::new(&project_path).components().next();
|
||||
if let Some(Component::CurDir | Component::Normal(_)) = path
|
||||
{
|
||||
let path = profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(&project_path);
|
||||
cache_file_hash(
|
||||
file.clone(),
|
||||
&profile_path,
|
||||
project.path.as_str(),
|
||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||
ProjectType::get_from_parent_folder(&path),
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
cache_file_hash(
|
||||
file.clone(),
|
||||
&profile_path,
|
||||
&project_path,
|
||||
project
|
||||
.hashes
|
||||
.get(&PackFileHash::Sha1)
|
||||
.map(|x| &**x),
|
||||
ProjectType::get_from_parent_folder(&path),
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
write(&path, &file, &state.io_semaphore).await?;
|
||||
|
||||
write(&path, &file, &state.io_semaphore).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
@@ -377,9 +368,10 @@ pub async fn remove_all_related_files(
|
||||
if let Some(metadata) = &project.metadata
|
||||
&& to_remove.contains(&metadata.project_id)
|
||||
{
|
||||
let path = profile_full_path.join(file_path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
match io::remove_file(profile_full_path.join(file_path)).await {
|
||||
Ok(_) => (),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,9 +379,12 @@ pub async fn remove_all_related_files(
|
||||
// Iterate over all Modrinth project file paths in the json, and remove them
|
||||
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
|
||||
for file in pack.files {
|
||||
let path: PathBuf = profile_full_path.join(file.path);
|
||||
if path.exists() {
|
||||
io::remove_file(&path).await?;
|
||||
match io::remove_file(profile_full_path.join(file.path.as_str()))
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,11 +407,16 @@ pub async fn remove_all_related_files(
|
||||
}
|
||||
|
||||
// Remove this file if a corresponding one exists in the filesystem
|
||||
let existing_file = profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(&new_path);
|
||||
if existing_file.exists() {
|
||||
io::remove_file(&existing_file).await?;
|
||||
match io::remove_file(
|
||||
profile::get_full_path(&profile_path)
|
||||
.await?
|
||||
.join(&new_path),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::util::io::{self, IOError};
|
||||
pub use crate::{State, state::Profile};
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use path_util::SafeRelativeUtf8UnixPathBuf;
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -497,11 +498,12 @@ pub async fn export_mrpack(
|
||||
let version_id = version_id.unwrap_or("1.0.0".to_string());
|
||||
let mut packfile =
|
||||
create_mrpack_json(&profile, version_id, description).await?;
|
||||
let included_candidates_set =
|
||||
HashSet::<_>::from_iter(included_export_candidates.iter());
|
||||
let included_candidates_set = HashSet::<_>::from_iter(
|
||||
included_export_candidates.iter().map(|x| x.as_str()),
|
||||
);
|
||||
packfile
|
||||
.files
|
||||
.retain(|f| included_candidates_set.contains(&f.path));
|
||||
.retain(|f| included_candidates_set.contains(f.path.as_str()));
|
||||
|
||||
// Build vec of all files in the folder
|
||||
let mut path_list = Vec::new();
|
||||
@@ -575,8 +577,8 @@ pub async fn export_mrpack(
|
||||
#[tracing::instrument]
|
||||
pub async fn get_pack_export_candidates(
|
||||
profile_path: &str,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut path_list: Vec<String> = Vec::new();
|
||||
) -> crate::Result<Vec<SafeRelativeUtf8UnixPathBuf>> {
|
||||
let mut path_list = Vec::new();
|
||||
|
||||
let profile_base_dir = get_full_path(profile_path).await?;
|
||||
let mut read_dir = io::read_dir(&profile_base_dir).await?;
|
||||
@@ -610,18 +612,19 @@ pub async fn get_pack_export_candidates(
|
||||
fn pack_get_relative_path(
|
||||
profile_path: &PathBuf,
|
||||
path: &PathBuf,
|
||||
) -> crate::Result<String> {
|
||||
Ok(path
|
||||
.strip_prefix(profile_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile"
|
||||
))
|
||||
})?
|
||||
.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"))
|
||||
) -> crate::Result<SafeRelativeUtf8UnixPathBuf> {
|
||||
Ok(SafeRelativeUtf8UnixPathBuf::try_from(
|
||||
path.strip_prefix(profile_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::FSError(format!(
|
||||
"Path {path:?} does not correspond to a profile"
|
||||
))
|
||||
})?
|
||||
.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"),
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Run Minecraft using a profile and the default credentials, logged in credentials,
|
||||
@@ -896,7 +899,15 @@ pub async fn create_mrpack_json(
|
||||
.collect();
|
||||
|
||||
Some(Ok(PackFile {
|
||||
path,
|
||||
path: match path.try_into() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
return Some(Err(crate::ErrorKind::OtherError(
|
||||
"Invalid file path in project".into(),
|
||||
)
|
||||
.as_error()));
|
||||
}
|
||||
},
|
||||
hashes,
|
||||
env: Some(env),
|
||||
downloads,
|
||||
|
||||
@@ -120,11 +120,11 @@ fn parse_server_address_inner(
|
||||
let mut port = None;
|
||||
if !port_str.is_empty() {
|
||||
if port_str.starts_with('+') {
|
||||
return Err(format!("Unparseable port number: {port_str}"));
|
||||
return Err(format!("Unparsable port number: {port_str}"));
|
||||
}
|
||||
port = port_str.parse::<u16>().ok();
|
||||
if port.is_none() {
|
||||
return Err(format!("Unparseable port number: {port_str}"));
|
||||
return Err(format!("Unparsable port number: {port_str}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,17 @@ use std::sync::Arc;
|
||||
|
||||
use crate::{profile, util};
|
||||
use data_url::DataUrlError;
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Display)]
|
||||
#[display("{description}")]
|
||||
pub struct LabrinthError {
|
||||
pub error: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
#[error("Filesystem error: {0}")]
|
||||
@@ -56,6 +65,9 @@ pub enum ErrorKind {
|
||||
#[error("Error fetching URL: {0}")]
|
||||
FetchError(#[from] reqwest::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
LabrinthError(LabrinthError),
|
||||
|
||||
#[error("Websocket error: {0}")]
|
||||
WSError(#[from] async_tungstenite::tungstenite::Error),
|
||||
|
||||
@@ -186,6 +198,18 @@ pub enum ErrorKind {
|
||||
ParseError {
|
||||
reason: String,
|
||||
},
|
||||
#[error("RPC error: {0}")]
|
||||
RpcError(String),
|
||||
|
||||
#[cfg(windows)]
|
||||
#[error("Windows error: {0}")]
|
||||
WindowsError(#[from] windows_core::Error),
|
||||
|
||||
#[error("zbus error: {0}")]
|
||||
ZbusError(#[from] zbus::Error),
|
||||
|
||||
#[error("Deserialization error: {0}")]
|
||||
DeserializationError(#[from] serde::de::value::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -16,14 +16,14 @@ use uuid::Uuid;
|
||||
const CLI_PROGRESS_BAR_TOTAL: u64 = 1000;
|
||||
|
||||
/*
|
||||
Events are a way we can communciate with the Tauri frontend from the Rust backend.
|
||||
Events are a way we can communicate with the Tauri frontend from the Rust backend.
|
||||
We include a feature flag for Tauri, so that we can compile this code without Tauri.
|
||||
|
||||
To use events, we need to do the following:
|
||||
1) Make sure we are using the tauri feature flag
|
||||
2) Initialize the EventState with EventState::init() *before* initializing the theseus State
|
||||
3) Call emit_x functions to send events to the frontend
|
||||
For emit_loading() specifically, we need to inialize the loading bar with init_loading() first and pass the received loader in
|
||||
For emit_loading() specifically, we need to initialize the loading bar with init_loading() first and pass the received loader in
|
||||
|
||||
For example:
|
||||
pub async fn loading_function() -> crate::Result<()> {
|
||||
@@ -306,7 +306,7 @@ pub async fn emit_friend(payload: FriendPayload) -> crate::Result<()> {
|
||||
// loading_join! macro
|
||||
// loading_join!(key: Option<&LoadingBarId>, total: f64, message: Option<&str>; task1, task2, task3...)
|
||||
// This will submit a loading event with the given message for each task as they complete
|
||||
// task1, task2, task3 are async tasks that yuo want to to join on await on
|
||||
// task1, task2, task3 are async tasks that you want to to join on await on
|
||||
// Key is the key to use for which loading bar to submit these results to- a LoadingBarId. If None, it does nothing
|
||||
// Total is the total amount of progress that the loading bar should take up by all futures in this (will be split evenly amongst them).
|
||||
// If message is Some(t) you will overwrite this loading bar's message with a custom one
|
||||
|
||||
@@ -179,7 +179,6 @@ pub enum LoadingBarType {
|
||||
CurseForgeProfileDownload {
|
||||
profile_name: String,
|
||||
},
|
||||
CheckingForUpdates,
|
||||
LauncherUpdate {
|
||||
version: String,
|
||||
current_version: String,
|
||||
|
||||
@@ -14,8 +14,9 @@ use daedalus::{
|
||||
modded::SidedDataEntry,
|
||||
};
|
||||
use dunce::canonicalize;
|
||||
use hashlink::LinkedHashSet;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use itertools::Itertools;
|
||||
use std::io::{BufRead, BufReader, ErrorKind};
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -29,9 +30,21 @@ pub fn get_class_paths(
|
||||
java_arch: &str,
|
||||
minecraft_updated: bool,
|
||||
) -> crate::Result<String> {
|
||||
let mut cps = libraries
|
||||
launcher_class_path
|
||||
.iter()
|
||||
.filter_map(|library| {
|
||||
.map(|path| {
|
||||
Ok(canonicalize(path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string())
|
||||
})
|
||||
.chain(libraries.iter().filter_map(|library| {
|
||||
if let Some(rules) = &library.rules
|
||||
&& !parse_rules(
|
||||
rules,
|
||||
@@ -47,29 +60,15 @@ pub fn get_class_paths(
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(get_lib_path(libraries_path, &library.name, false))
|
||||
Some(get_lib_path(
|
||||
libraries_path,
|
||||
&library.name,
|
||||
library.natives_os_key_and_classifiers(java_arch).is_some(),
|
||||
))
|
||||
}))
|
||||
.process_results(|iter| {
|
||||
iter.unique().join(classpath_separator(java_arch))
|
||||
})
|
||||
.collect::<Result<LinkedHashSet<_>, _>>()?;
|
||||
|
||||
for launcher_path in launcher_class_path {
|
||||
cps.insert(
|
||||
canonicalize(launcher_path)
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Specified class path {} does not exist",
|
||||
launcher_path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(cps
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(classpath_separator(java_arch)))
|
||||
}
|
||||
|
||||
pub fn get_class_paths_jar<T: AsRef<str>>(
|
||||
@@ -90,21 +89,21 @@ pub fn get_lib_path(
|
||||
lib: &str,
|
||||
allow_not_exist: bool,
|
||||
) -> crate::Result<String> {
|
||||
let path = libraries_path
|
||||
.to_path_buf()
|
||||
.join(get_path_from_artifact(lib)?);
|
||||
let path = libraries_path.join(get_path_from_artifact(lib)?);
|
||||
|
||||
if !path.exists() && allow_not_exist {
|
||||
return Ok(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
let path = &canonicalize(&path).map_err(|_| {
|
||||
crate::ErrorKind::LauncherError(format!(
|
||||
"Library file at path {} does not exist",
|
||||
path.to_string_lossy()
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
let path = match canonicalize(&path) {
|
||||
Ok(p) => p,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound && allow_not_exist => {
|
||||
path
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(crate::ErrorKind::LauncherError(format!(
|
||||
"Could not canonicalize library path {}: {err}",
|
||||
path.display()
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
}
|
||||
@@ -124,6 +123,7 @@ pub fn get_jvm_arguments(
|
||||
quick_play_type: &QuickPlayType,
|
||||
quick_play_version: QuickPlayVersion,
|
||||
log_config: Option<&LoggingConfiguration>,
|
||||
ipc_addr: SocketAddr,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let mut parsed_arguments = Vec::new();
|
||||
|
||||
@@ -181,6 +181,11 @@ pub fn get_jvm_arguments(
|
||||
.to_string_lossy()
|
||||
));
|
||||
|
||||
parsed_arguments
|
||||
.push(format!("-Dmodrinth.internal.ipc.host={}", ipc_addr.ip()));
|
||||
parsed_arguments
|
||||
.push(format!("-Dmodrinth.internal.ipc.port={}", ipc_addr.port()));
|
||||
|
||||
parsed_arguments.push(format!(
|
||||
"-Dmodrinth.internal.quickPlay.serverVersion={}",
|
||||
serde_json::to_value(quick_play_version.server)?
|
||||
|
||||
@@ -8,13 +8,13 @@ use crate::{
|
||||
emit::{emit_loading, loading_try_for_each_concurrent},
|
||||
},
|
||||
state::State,
|
||||
util::{fetch::*, io, platform::OsExt},
|
||||
util::{fetch::*, io},
|
||||
};
|
||||
use daedalus::minecraft::{LoggingConfiguration, LoggingSide};
|
||||
use daedalus::{
|
||||
self as d,
|
||||
minecraft::{
|
||||
Asset, AssetsIndex, Library, Os, Version as GameVersion,
|
||||
Asset, AssetsIndex, Library, Version as GameVersion,
|
||||
VersionInfo as GameVersionInfo,
|
||||
},
|
||||
modded::LoaderVersion,
|
||||
@@ -288,90 +288,132 @@ pub async fn download_libraries(
|
||||
}?;
|
||||
let num_files = libraries.len();
|
||||
loading_try_for_each_concurrent(
|
||||
stream::iter(libraries.iter())
|
||||
.map(Ok::<&Library, crate::Error>), None, loading_bar,loading_amount,num_files, None,|library| async move {
|
||||
if let Some(rules) = &library.rules
|
||||
&& !parse_rules(rules, java_arch, &QuickPlayType::None, minecraft_updated) {
|
||||
tracing::trace!("Skipped library {}", &library.name);
|
||||
return Ok(());
|
||||
}
|
||||
stream::iter(libraries.iter()).map(Ok::<&Library, crate::Error>),
|
||||
None,
|
||||
loading_bar,
|
||||
loading_amount,
|
||||
num_files,
|
||||
None,
|
||||
|library| async move {
|
||||
if let Some(rules) = &library.rules
|
||||
&& !parse_rules(
|
||||
rules,
|
||||
java_arch,
|
||||
&QuickPlayType::None,
|
||||
minecraft_updated,
|
||||
)
|
||||
{
|
||||
tracing::trace!("Skipped library {}", &library.name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !library.downloadable {
|
||||
tracing::trace!("Skipped non-downloadable library {}", &library.name);
|
||||
if !library.downloadable {
|
||||
tracing::trace!(
|
||||
"Skipped non-downloadable library {}",
|
||||
&library.name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// When a library has natives, we only need to download such natives, as PrismLauncher does
|
||||
if let Some((os_key, classifiers)) =
|
||||
library.natives_os_key_and_classifiers(java_arch)
|
||||
{
|
||||
let parsed_key = os_key
|
||||
.replace("${arch}", crate::util::platform::ARCH_WIDTH);
|
||||
|
||||
if let Some(native) = classifiers.get(&parsed_key) {
|
||||
let data = fetch(
|
||||
&native.url,
|
||||
Some(&native.sha1),
|
||||
&st.fetch_semaphore,
|
||||
&st.pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Ok(mut archive) =
|
||||
zip::ZipArchive::new(std::io::Cursor::new(&data))
|
||||
{
|
||||
match archive.extract(
|
||||
st.directories.version_natives_dir(version),
|
||||
) {
|
||||
Ok(_) => tracing::debug!(
|
||||
"Fetched native {}",
|
||||
&library.name
|
||||
),
|
||||
Err(err) => tracing::error!(
|
||||
"Failed extracting native {}. err: {err}",
|
||||
&library.name
|
||||
),
|
||||
}
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed extracting native {}",
|
||||
&library.name
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let artifact_path = d::get_path_from_artifact(&library.name)?;
|
||||
let path = st.directories.libraries_dir().join(&artifact_path);
|
||||
|
||||
if path.exists() && !force {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tokio::try_join! {
|
||||
async {
|
||||
let artifact_path = d::get_path_from_artifact(&library.name)?;
|
||||
let path = st.directories.libraries_dir().join(&artifact_path);
|
||||
if let Some(d::minecraft::LibraryDownloads {
|
||||
artifact: Some(ref artifact),
|
||||
..
|
||||
}) = library.downloads
|
||||
&& !artifact.url.is_empty()
|
||||
{
|
||||
let bytes = fetch(
|
||||
&artifact.url,
|
||||
Some(&artifact.sha1),
|
||||
&st.fetch_semaphore,
|
||||
&st.pool,
|
||||
)
|
||||
.await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
|
||||
if path.exists() && !force {
|
||||
return Ok(());
|
||||
}
|
||||
tracing::trace!(
|
||||
"Fetched library {} to path {:?}",
|
||||
&library.name,
|
||||
&path
|
||||
);
|
||||
} else {
|
||||
// We lack an artifact URL, so fall back to constructing one ourselves.
|
||||
// PrismLauncher just ignores the library if this is the case, so it's
|
||||
// probably not needed, but previous code revisions of the Modrinth App
|
||||
// intended to do this, so we keep that behavior for compatibility.
|
||||
|
||||
if let Some(d::minecraft::LibraryDownloads { artifact: Some(ref artifact), ..}) = library.downloads
|
||||
&& !artifact.url.is_empty(){
|
||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &st.pool)
|
||||
.await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||
return Ok::<_, crate::Error>(());
|
||||
}
|
||||
let url = format!(
|
||||
"{}{artifact_path}",
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net/")
|
||||
);
|
||||
|
||||
let url = [
|
||||
library
|
||||
.url
|
||||
.as_deref()
|
||||
.unwrap_or("https://libraries.minecraft.net/"),
|
||||
&artifact_path
|
||||
].concat();
|
||||
let bytes =
|
||||
fetch(&url, None, &st.fetch_semaphore, &st.pool)
|
||||
.await?;
|
||||
|
||||
let bytes = fetch(&url, None, &st.fetch_semaphore, &st.pool).await?;
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||
Ok::<_, crate::Error>(())
|
||||
},
|
||||
async {
|
||||
// HACK: pseudo try block using or else
|
||||
if let Some((os_key, classifiers)) = None.or_else(|| Some((
|
||||
library
|
||||
.natives
|
||||
.as_ref()?
|
||||
.get(&Os::native_arch(java_arch))?,
|
||||
library
|
||||
.downloads
|
||||
.as_ref()?
|
||||
.classifiers
|
||||
.as_ref()?
|
||||
))) {
|
||||
let parsed_key = os_key.replace(
|
||||
"${arch}",
|
||||
crate::util::platform::ARCH_WIDTH,
|
||||
);
|
||||
write(&path, &bytes, &st.io_semaphore).await?;
|
||||
|
||||
if let Some(native) = classifiers.get(&parsed_key) {
|
||||
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore, &st.pool).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)) {
|
||||
Ok(_) => tracing::debug!("Fetched native {}", &library.name),
|
||||
Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err)
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Failed extracting native {}", &library.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}?;
|
||||
|
||||
tracing::debug!("Loaded library {}", library.name);
|
||||
Ok(())
|
||||
tracing::trace!(
|
||||
"Fetched library {} to path {:?}",
|
||||
&library.name,
|
||||
&path
|
||||
);
|
||||
}
|
||||
}
|
||||
).await?;
|
||||
|
||||
tracing::debug!("Loaded library {}", library.name);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::debug!("Done loading libraries!");
|
||||
Ok(())
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::state::{
|
||||
AccountType, Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
||||
};
|
||||
use crate::util::{io, utils};
|
||||
use crate::util::rpc::RpcServerBuilder;
|
||||
use crate::{State, get_resource_file, process, state as st};
|
||||
use chrono::Utc;
|
||||
use daedalus as d;
|
||||
@@ -23,7 +24,6 @@ use serde::Deserialize;
|
||||
use st::Profile;
|
||||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
mod args;
|
||||
@@ -611,6 +611,8 @@ pub async fn launch_minecraft(
|
||||
let (main_class_keep_alive, main_class_path) =
|
||||
get_resource_file!(env "JAVA_JARS_DIR" / "theseus.jar")?;
|
||||
|
||||
let rpc_server = RpcServerBuilder::new().launch().await?;
|
||||
|
||||
command.args(
|
||||
args::get_jvm_arguments(
|
||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||
@@ -636,6 +638,7 @@ pub async fn launch_minecraft(
|
||||
.logging
|
||||
.as_ref()
|
||||
.and_then(|x| x.get(&LoggingSide::Client)),
|
||||
rpc_server.address(),
|
||||
)?
|
||||
.into_iter(),
|
||||
);
|
||||
@@ -800,7 +803,8 @@ pub async fn launch_minecraft(
|
||||
state.directories.profile_logs_dir(&profile.path),
|
||||
version_info.logging.is_some(),
|
||||
main_class_keep_alive,
|
||||
async |process: &ProcessMetadata, stdin| {
|
||||
rpc_server,
|
||||
async |process: &ProcessMetadata, rpc_server| {
|
||||
let process_start_time = process.start_time.to_rfc3339();
|
||||
let profile_created_time = profile.created.to_rfc3339();
|
||||
let profile_modified_time = profile.modified.to_rfc3339();
|
||||
@@ -823,14 +827,11 @@ pub async fn launch_minecraft(
|
||||
let Some(value) = value else {
|
||||
continue;
|
||||
};
|
||||
stdin.write_all(b"property\t").await?;
|
||||
stdin.write_all(key.as_bytes()).await?;
|
||||
stdin.write_u8(b'\t').await?;
|
||||
stdin.write_all(value.as_bytes()).await?;
|
||||
stdin.write_u8(b'\n').await?;
|
||||
rpc_server
|
||||
.call_method_2::<()>("set_system_property", key, value)
|
||||
.await?;
|
||||
}
|
||||
stdin.write_all(b"launch\n").await?;
|
||||
stdin.flush().await?;
|
||||
rpc_server.call_method::<()>("launch").await?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
|
||||
@@ -25,3 +25,9 @@ pub use event::{
|
||||
};
|
||||
pub use logger::start_logger;
|
||||
pub use state::State;
|
||||
|
||||
pub const LAUNCHER_USER_AGENT: &str = concat!(
|
||||
"modrinth/theseus/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (support@modrinth.com)"
|
||||
);
|
||||
|
||||
@@ -25,12 +25,11 @@ pub fn start_logger() -> Option<()> {
|
||||
.unwrap_or_else(|_| {
|
||||
tracing_subscriber::EnvFilter::new("theseus=info,theseus_gui=info")
|
||||
});
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(filter)
|
||||
.with(tracing_error::ErrorLayer::default());
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("setting default subscriber failed");
|
||||
.with(tracing_error::ErrorLayer::default())
|
||||
.init();
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -76,7 +75,7 @@ pub fn start_logger() -> Option<()> {
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(file)
|
||||
@@ -84,10 +83,8 @@ pub fn start_logger() -> Option<()> {
|
||||
.with_timer(ChronoLocal::rfc_3339()),
|
||||
)
|
||||
.with(filter)
|
||||
.with(tracing_error::ErrorLayer::default());
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("Setting default subscriber failed");
|
||||
.with(tracing_error::ErrorLayer::default())
|
||||
.init();
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -519,11 +519,14 @@ impl CacheValue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Copy, Clone)]
|
||||
#[derive(
|
||||
Deserialize, Serialize, PartialEq, Eq, Debug, Copy, Clone, Default,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CacheBehaviour {
|
||||
/// Serve expired data. If fetch fails / launcher is offline, errors are ignored
|
||||
/// and expired data is served
|
||||
#[default]
|
||||
StaleWhileRevalidateSkipOffline,
|
||||
// Serve expired data, revalidate in background
|
||||
StaleWhileRevalidate,
|
||||
@@ -533,12 +536,6 @@ pub enum CacheBehaviour {
|
||||
Bypass,
|
||||
}
|
||||
|
||||
impl Default for CacheBehaviour {
|
||||
fn default() -> Self {
|
||||
Self::StaleWhileRevalidateSkipOffline
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedEntry {
|
||||
id: String,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::ErrorKind;
|
||||
use crate::LAUNCHER_USER_AGENT;
|
||||
use crate::data::ModrinthCredentials;
|
||||
use crate::event::FriendPayload;
|
||||
use crate::event::emit::emit_friend;
|
||||
@@ -81,13 +83,9 @@ impl FriendsSocket {
|
||||
)
|
||||
.into_client_request()?;
|
||||
|
||||
let user_agent = format!(
|
||||
"modrinth/theseus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
request.headers_mut().insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str(&user_agent).unwrap(),
|
||||
HeaderValue::from_str(LAUNCHER_USER_AGENT).unwrap(),
|
||||
);
|
||||
|
||||
let res = connect_async(request).await;
|
||||
@@ -322,7 +320,7 @@ impl FriendsSocket {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
fetch_advanced(
|
||||
let result = fetch_advanced(
|
||||
Method::POST,
|
||||
&format!("{}friend/{user_id}", env!("MODRINTH_API_URL_V3")),
|
||||
None,
|
||||
@@ -332,7 +330,18 @@ impl FriendsSocket {
|
||||
semaphore,
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
if let Err(ref e) = result
|
||||
&& let ErrorKind::LabrinthError(e) = &*e.raw
|
||||
&& e.error == "not_found"
|
||||
{
|
||||
return Err(ErrorKind::OtherError(format!(
|
||||
"No user found with username \"{user_id}\""
|
||||
))
|
||||
.into());
|
||||
}
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub async fn init_watcher() -> crate::Result<FileWatcher> {
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let span = tracing::span!(tracing::Level::INFO, "init_watcher");
|
||||
tracing::info!(parent: &span, "Initting watcher");
|
||||
tracing::info!(parent: &span, "Initing watcher");
|
||||
while let Some(res) = rx.recv().await {
|
||||
let _span = span.enter();
|
||||
|
||||
@@ -170,38 +170,22 @@ pub(crate) async fn watch_profile(
|
||||
let profile_path = dirs.profiles_dir().join(profile_path);
|
||||
|
||||
if profile_path.exists() && profile_path.is_dir() {
|
||||
for sub_path in ProjectType::iterator().map(|x| x.get_folder()).chain([
|
||||
"crash-reports",
|
||||
"saves",
|
||||
"servers.dat",
|
||||
]) {
|
||||
for sub_path in ProjectType::iterator()
|
||||
.map(|x| x.get_folder())
|
||||
.chain(["crash-reports", "saves"])
|
||||
{
|
||||
let full_path = profile_path.join(sub_path);
|
||||
|
||||
if !full_path.exists() && !full_path.is_symlink() {
|
||||
if !sub_path.contains(".") {
|
||||
if let Err(e) =
|
||||
crate::util::io::create_dir_all(&full_path).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to create directory for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if sub_path == "servers.dat" {
|
||||
const EMPTY_NBT: &[u8] = &[
|
||||
10, // Compound tag
|
||||
0, 0, // Empty name
|
||||
0, // End of compound tag
|
||||
];
|
||||
if let Err(e) =
|
||||
crate::util::io::write(&full_path, EMPTY_NBT).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to create file for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if !full_path.exists()
|
||||
&& !full_path.is_symlink()
|
||||
&& !sub_path.contains(".")
|
||||
&& let Err(e) =
|
||||
crate::util::io::create_dir_all(&full_path).await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to create directory for watcher {full_path:?}: {e}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut watcher = watcher.write().await;
|
||||
@@ -215,6 +199,16 @@ pub(crate) async fn watch_profile(
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut watcher = watcher.write().await;
|
||||
if let Err(e) = watcher
|
||||
.watcher()
|
||||
.watch(&profile_path, RecursiveMode::NonRecursive)
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to watch root profile directory for watcher {profile_path:?}: {e}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::event::emit::{emit_process, emit_profile};
|
||||
use crate::event::{ProcessPayloadType, ProfilePayloadType};
|
||||
use crate::profile;
|
||||
use crate::util::io::IOError;
|
||||
use crate::util::rpc::RpcServer;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use quick_xml::Reader;
|
||||
@@ -15,7 +16,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::{Child, ChildStdin, Command};
|
||||
use tokio::process::{Child, Command};
|
||||
use uuid::Uuid;
|
||||
|
||||
const LAUNCHER_LOG_PATH: &str = "launcher_log.txt";
|
||||
@@ -46,9 +47,10 @@ impl ProcessManager {
|
||||
logs_folder: PathBuf,
|
||||
xml_logging: bool,
|
||||
main_class_keep_alive: TempDir,
|
||||
rpc_server: RpcServer,
|
||||
post_process_init: impl AsyncFnOnce(
|
||||
&ProcessMetadata,
|
||||
&mut ChildStdin,
|
||||
&RpcServer,
|
||||
) -> crate::Result<()>,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
mc_command.stdout(std::process::Stdio::piped());
|
||||
@@ -67,14 +69,12 @@ impl ProcessManager {
|
||||
profile_path: profile_path.to_string(),
|
||||
},
|
||||
child: mc_proc,
|
||||
rpc_server,
|
||||
_main_class_keep_alive: main_class_keep_alive,
|
||||
};
|
||||
|
||||
if let Err(e) = post_process_init(
|
||||
&process.metadata,
|
||||
&mut process.child.stdin.as_mut().unwrap(),
|
||||
)
|
||||
.await
|
||||
if let Err(e) =
|
||||
post_process_init(&process.metadata, &process.rpc_server).await
|
||||
{
|
||||
tracing::error!("Failed to run post-process init: {e}");
|
||||
let _ = process.child.kill().await;
|
||||
@@ -165,6 +165,10 @@ impl ProcessManager {
|
||||
self.processes.get(&id).map(|x| x.metadata.clone())
|
||||
}
|
||||
|
||||
pub fn get_rpc(&self, id: Uuid) -> Option<RpcServer> {
|
||||
self.processes.get(&id).map(|x| x.rpc_server.clone())
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> Vec<ProcessMetadata> {
|
||||
self.processes
|
||||
.iter()
|
||||
@@ -215,6 +219,7 @@ struct Process {
|
||||
metadata: ProcessMetadata,
|
||||
child: Child,
|
||||
_main_class_keep_alive: TempDir,
|
||||
rpc_server: RpcServer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -38,6 +38,10 @@ pub struct Settings {
|
||||
|
||||
pub developer_mode: bool,
|
||||
pub feature_flags: HashMap<FeatureFlag, bool>,
|
||||
|
||||
pub skipped_update: Option<String>,
|
||||
pub pending_update_toast_for_version: Option<String>,
|
||||
pub auto_download_updates: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
@@ -63,7 +67,8 @@ impl Settings {
|
||||
json(extra_launch_args) extra_launch_args, json(custom_env_vars) custom_env_vars,
|
||||
mc_memory_max, mc_force_fullscreen, mc_game_resolution_x, mc_game_resolution_y, hide_on_process_start,
|
||||
hook_pre_launch, hook_wrapper, hook_post_exit,
|
||||
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar
|
||||
custom_dir, prev_custom_dir, migrated, json(feature_flags) feature_flags, toggle_sidebar,
|
||||
skipped_update, pending_update_toast_for_version, auto_download_updates
|
||||
FROM settings
|
||||
"
|
||||
)
|
||||
@@ -117,6 +122,10 @@ impl Settings {
|
||||
.as_ref()
|
||||
.and_then(|x| serde_json::from_str(x).ok())
|
||||
.unwrap_or_default(),
|
||||
skipped_update: res.skipped_update,
|
||||
pending_update_toast_for_version: res
|
||||
.pending_update_toast_for_version,
|
||||
auto_download_updates: res.auto_download_updates.map(|x| x == 1),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,7 +179,11 @@ impl Settings {
|
||||
|
||||
toggle_sidebar = $26,
|
||||
feature_flags = $27,
|
||||
hide_nametag_skins_page = $28
|
||||
hide_nametag_skins_page = $28,
|
||||
|
||||
skipped_update = $29,
|
||||
pending_update_toast_for_version = $30,
|
||||
auto_download_updates = $31
|
||||
",
|
||||
max_concurrent_writes,
|
||||
max_concurrent_downloads,
|
||||
@@ -199,7 +212,10 @@ impl Settings {
|
||||
self.migrated,
|
||||
self.toggle_sidebar,
|
||||
feature_flags,
|
||||
self.hide_nametag_skins_page
|
||||
self.hide_nametag_skins_page,
|
||||
self.skipped_update,
|
||||
self.pending_update_toast_for_version,
|
||||
self.auto_download_updates,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Functions for fetching information from the Internet
|
||||
use super::io::{self, IOError};
|
||||
use crate::ErrorKind;
|
||||
use crate::LAUNCHER_USER_AGENT;
|
||||
use crate::event::LoadingBarId;
|
||||
use crate::event::emit::emit_loading;
|
||||
use bytes::Bytes;
|
||||
@@ -19,11 +21,8 @@ pub struct FetchSemaphore(pub Semaphore);
|
||||
|
||||
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
let header = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"modrinth/theseus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.unwrap();
|
||||
let header =
|
||||
reqwest::header::HeaderValue::from_str(LAUNCHER_USER_AGENT).unwrap();
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(time::Duration::from_secs(10)))
|
||||
@@ -108,32 +107,31 @@ pub async fn fetch_advanced(
|
||||
|
||||
let result = req.send().await;
|
||||
match result {
|
||||
Ok(x) => {
|
||||
if x.status().is_server_error() {
|
||||
if attempt <= FETCH_ATTEMPTS {
|
||||
continue;
|
||||
} else {
|
||||
return Err(crate::Error::from(
|
||||
crate::ErrorKind::OtherError(
|
||||
"Server error when fetching content"
|
||||
.to_string(),
|
||||
),
|
||||
));
|
||||
Ok(resp) => {
|
||||
if resp.status().is_server_error() && attempt <= FETCH_ATTEMPTS
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if resp.status().is_client_error()
|
||||
|| resp.status().is_server_error()
|
||||
{
|
||||
let backup_error = resp.error_for_status_ref().unwrap_err();
|
||||
if let Ok(error) = resp.json().await {
|
||||
return Err(ErrorKind::LabrinthError(error).into());
|
||||
}
|
||||
return Err(backup_error.into());
|
||||
}
|
||||
|
||||
let bytes = if let Some((bar, total)) = &loading_bar {
|
||||
let length = x.content_length();
|
||||
let length = resp.content_length();
|
||||
if let Some(total_size) = length {
|
||||
use futures::StreamExt;
|
||||
let mut stream = x.bytes_stream();
|
||||
let mut stream = resp.bytes_stream();
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = item.or(Err(
|
||||
crate::error::ErrorKind::NoValueFor(
|
||||
"fetch bytes".to_string(),
|
||||
),
|
||||
))?;
|
||||
let chunk = item.or(Err(ErrorKind::NoValueFor(
|
||||
"fetch bytes".to_string(),
|
||||
)))?;
|
||||
bytes.append(&mut chunk.to_vec());
|
||||
emit_loading(
|
||||
bar,
|
||||
@@ -145,10 +143,10 @@ pub async fn fetch_advanced(
|
||||
|
||||
Ok(bytes::Bytes::from(bytes))
|
||||
} else {
|
||||
x.bytes().await
|
||||
resp.bytes().await
|
||||
}
|
||||
} else {
|
||||
x.bytes().await
|
||||
resp.bytes().await
|
||||
};
|
||||
|
||||
if let Ok(bytes) = bytes {
|
||||
@@ -158,7 +156,7 @@ pub async fn fetch_advanced(
|
||||
if attempt <= FETCH_ATTEMPTS {
|
||||
continue;
|
||||
} else {
|
||||
return Err(crate::ErrorKind::HashError(
|
||||
return Err(ErrorKind::HashError(
|
||||
sha1.to_string(),
|
||||
hash,
|
||||
)
|
||||
@@ -194,10 +192,9 @@ pub async fn fetch_mirrors(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<Bytes> {
|
||||
if mirrors.is_empty() {
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"No mirrors provided!".to_string(),
|
||||
)
|
||||
.into());
|
||||
return Err(
|
||||
ErrorKind::InputError("No mirrors provided!".to_string()).into()
|
||||
);
|
||||
}
|
||||
|
||||
for (index, mirror) in mirrors.iter().enumerate() {
|
||||
@@ -276,8 +273,8 @@ pub async fn write(
|
||||
}
|
||||
|
||||
pub async fn copy(
|
||||
src: impl AsRef<std::path::Path>,
|
||||
dest: impl AsRef<std::path::Path>,
|
||||
src: impl AsRef<Path>,
|
||||
dest: impl AsRef<Path>,
|
||||
semaphore: &IoSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
let src: &Path = src.as_ref();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// IO error
|
||||
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
|
||||
|
||||
use std::{io::Write, path::Path};
|
||||
use std::{
|
||||
io::{ErrorKind, Write},
|
||||
path::Path,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
||||
@@ -32,6 +35,13 @@ impl IOError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
match self {
|
||||
IOError::IOPathError { source, .. } => source.kind(),
|
||||
IOError::IOError(source) => source.kind(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canonicalize(
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
pub mod fetch;
|
||||
pub mod io;
|
||||
pub mod jre;
|
||||
pub mod network;
|
||||
pub mod platform;
|
||||
pub mod utils; // [AR] Feature
|
||||
pub mod protocol_version;
|
||||
pub mod rpc;
|
||||
pub mod server_ping;
|
||||
|
||||
93
packages/app-lib/src/util/network.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::Result;
|
||||
use std::io;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
pub async fn tcp_listen_any_loopback() -> io::Result<TcpListener> {
|
||||
// IPv4 is tried first for the best compatibility and performance with most systems.
|
||||
// IPv6 is also tried in case IPv4 is not available. Resolving "localhost" is avoided
|
||||
// to prevent failures deriving from improper name resolution setup. Any available
|
||||
// ephemeral port is used to prevent conflicts with other services. This is all as per
|
||||
// RFC 8252's recommendations
|
||||
const ANY_LOOPBACK_SOCKET: &[SocketAddr] = &[
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
||||
];
|
||||
|
||||
TcpListener::bind(ANY_LOOPBACK_SOCKET).await
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn is_network_metered() -> Result<bool> {
|
||||
use windows::Networking::Connectivity::{
|
||||
NetworkCostType, NetworkInformation,
|
||||
};
|
||||
|
||||
let cost_type = NetworkInformation::GetInternetConnectionProfile()?
|
||||
.GetConnectionCost()?
|
||||
.NetworkCostType()?;
|
||||
Ok(matches!(
|
||||
cost_type,
|
||||
NetworkCostType::Fixed | NetworkCostType::Variable
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub async fn is_network_metered() -> Result<bool> {
|
||||
use crate::ErrorKind;
|
||||
use cidre::dispatch::Queue;
|
||||
use cidre::nw::PathMonitor;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::future::FutureExt;
|
||||
|
||||
let (sender, mut receiver) = mpsc::channel(1);
|
||||
|
||||
let queue = Queue::new();
|
||||
let mut monitor = PathMonitor::new();
|
||||
monitor.set_queue(&queue);
|
||||
monitor.set_update_handler(move |path| {
|
||||
let _ = sender.try_send(path.is_constrained() || path.is_expensive());
|
||||
});
|
||||
|
||||
monitor.start();
|
||||
let result = receiver
|
||||
.recv()
|
||||
.timeout(Duration::from_millis(100))
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
monitor.cancel();
|
||||
|
||||
result.ok_or_else(|| {
|
||||
ErrorKind::OtherError(
|
||||
"NWPathMonitor didn't provide an NWPath in time".to_string(),
|
||||
)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn is_network_metered() -> Result<bool> {
|
||||
// Thanks to https://github.com/Hakanbaban53/rclone-manager for showing how to do this
|
||||
use zbus::{Connection, Proxy};
|
||||
|
||||
let connection = Connection::system().await?;
|
||||
let proxy = Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.NetworkManager",
|
||||
"/org/freedesktop/NetworkManager",
|
||||
"org.freedesktop.NetworkManager",
|
||||
)
|
||||
.await?;
|
||||
let metered = proxy.get_property("Metered").await?;
|
||||
Ok(matches!(metered, 1 | 3))
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
|
||||
pub async fn is_network_metered() -> Result<bool> {
|
||||
tracing::warn!(
|
||||
"is_network_metered called on unsupported platform. Assuming unmetered."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
@@ -1,65 +1,6 @@
|
||||
//! Platform-related code
|
||||
use daedalus::minecraft::{Os, OsRule};
|
||||
|
||||
// OS detection
|
||||
pub trait OsExt {
|
||||
/// Get the OS of the current system
|
||||
fn native() -> Self;
|
||||
|
||||
/// Gets the OS + Arch of the current system
|
||||
fn native_arch(java_arch: &str) -> Self;
|
||||
|
||||
/// Gets the OS from an OS + Arch
|
||||
fn get_os(&self) -> Self;
|
||||
}
|
||||
|
||||
impl OsExt for Os {
|
||||
fn native() -> Self {
|
||||
match std::env::consts::OS {
|
||||
"windows" => Self::Windows,
|
||||
"macos" => Self::Osx,
|
||||
"linux" => Self::Linux,
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn native_arch(java_arch: &str) -> Self {
|
||||
if std::env::consts::OS == "windows" {
|
||||
if java_arch == "aarch64" {
|
||||
Os::WindowsArm64
|
||||
} else {
|
||||
Os::Windows
|
||||
}
|
||||
} else if std::env::consts::OS == "linux" {
|
||||
if java_arch == "aarch64" {
|
||||
Os::LinuxArm64
|
||||
} else if java_arch == "arm" {
|
||||
Os::LinuxArm32
|
||||
} else {
|
||||
Os::Linux
|
||||
}
|
||||
} else if std::env::consts::OS == "macos" {
|
||||
if java_arch == "aarch64" {
|
||||
Os::OsxArm64
|
||||
} else {
|
||||
Os::Osx
|
||||
}
|
||||
} else {
|
||||
Os::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn get_os(&self) -> Self {
|
||||
match self {
|
||||
Os::OsxArm64 => Os::Osx,
|
||||
Os::LinuxArm32 => Os::Linux,
|
||||
Os::LinuxArm64 => Os::Linux,
|
||||
Os::WindowsArm64 => Os::Windows,
|
||||
_ => self.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bit width
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
pub const ARCH_WIDTH: &str = "64";
|
||||
|
||||
258
packages/app-lib/src/util/rpc.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use crate::prelude::tcp_listen_any_loopback;
|
||||
use crate::{ErrorKind, Result};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::task::AbortHandle;
|
||||
use tokio_util::codec::{Decoder, LinesCodec, LinesCodecError};
|
||||
use uuid::Uuid;
|
||||
|
||||
type HandlerFuture = Pin<Box<dyn Send + Future<Output = Result<Value>>>>;
|
||||
type HandlerMethod = Box<dyn Send + Sync + Fn(Vec<Value>) -> HandlerFuture>;
|
||||
type HandlerMap = HashMap<&'static str, HandlerMethod>;
|
||||
type WaitingResponsesMap =
|
||||
Arc<Mutex<HashMap<Uuid, oneshot::Sender<Result<Value>>>>>;
|
||||
|
||||
pub struct RpcServerBuilder {
|
||||
handlers: HandlerMap,
|
||||
}
|
||||
|
||||
impl RpcServerBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
handlers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// We'll use this function in the future. Please remove this #[allow] when we do.
|
||||
#[allow(dead_code)]
|
||||
pub fn handler(
|
||||
mut self,
|
||||
function_name: &'static str,
|
||||
handler: HandlerMethod,
|
||||
) -> Self {
|
||||
self.handlers.insert(function_name, Box::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn launch(self) -> Result<RpcServer> {
|
||||
let socket = tcp_listen_any_loopback().await?;
|
||||
let address = socket.local_addr()?;
|
||||
let (message_sender, message_receiver) = mpsc::unbounded_channel();
|
||||
let waiting_responses = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let join_handle = {
|
||||
let waiting_responses = waiting_responses.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut server = RunningRpcServer {
|
||||
message_receiver,
|
||||
handlers: self.handlers,
|
||||
waiting_responses: waiting_responses.clone(),
|
||||
};
|
||||
if let Err(e) = server.run(socket).await {
|
||||
tracing::error!("Failed to run RPC server: {e}");
|
||||
}
|
||||
waiting_responses.lock().unwrap().clear();
|
||||
})
|
||||
};
|
||||
Ok(RpcServer {
|
||||
address,
|
||||
message_sender,
|
||||
waiting_responses,
|
||||
abort_handle: join_handle.abort_handle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RpcServer {
|
||||
address: SocketAddr,
|
||||
message_sender: mpsc::UnboundedSender<RpcMessage>,
|
||||
waiting_responses: WaitingResponsesMap,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
|
||||
impl RpcServer {
|
||||
pub fn address(&self) -> SocketAddr {
|
||||
self.address
|
||||
}
|
||||
|
||||
pub async fn call_method<R: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
) -> Result<R> {
|
||||
self.call_method_any(method, vec![]).await
|
||||
}
|
||||
|
||||
pub async fn call_method_2<R: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
arg1: impl Serialize,
|
||||
arg2: impl Serialize,
|
||||
) -> Result<R> {
|
||||
self.call_method_any(
|
||||
method,
|
||||
vec![serde_json::to_value(arg1)?, serde_json::to_value(arg2)?],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn call_method_any<R: DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
args: Vec<Value>,
|
||||
) -> Result<R> {
|
||||
if self.message_sender.is_closed() {
|
||||
return Err(ErrorKind::RpcError(
|
||||
"RPC connection closed".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
let (send, recv) = oneshot::channel();
|
||||
self.waiting_responses.lock().unwrap().insert(id, send);
|
||||
|
||||
let message = RpcMessage {
|
||||
id,
|
||||
body: RpcMessageBody::Call {
|
||||
method: method.to_owned(),
|
||||
args,
|
||||
},
|
||||
};
|
||||
if self.message_sender.send(message).is_err() {
|
||||
self.waiting_responses.lock().unwrap().remove(&id);
|
||||
return Err(ErrorKind::RpcError(
|
||||
"RPC connection closed while sending".to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
tracing::debug!("Waiting on result for {id}");
|
||||
let Ok(result) = recv.await else {
|
||||
self.waiting_responses.lock().unwrap().remove(&id);
|
||||
return Err(ErrorKind::RpcError(
|
||||
"RPC connection closed while waiting for response".to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
result.and_then(|x| Ok(serde_json::from_value(x)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RpcServer {
|
||||
fn drop(&mut self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
struct RunningRpcServer {
|
||||
message_receiver: mpsc::UnboundedReceiver<RpcMessage>,
|
||||
handlers: HandlerMap,
|
||||
waiting_responses: WaitingResponsesMap,
|
||||
}
|
||||
|
||||
impl RunningRpcServer {
|
||||
async fn run(&mut self, listener: TcpListener) -> Result<()> {
|
||||
let (socket, _) = listener.accept().await?;
|
||||
drop(listener);
|
||||
|
||||
let mut socket = LinesCodec::new().framed(socket);
|
||||
loop {
|
||||
let to_send = tokio::select! {
|
||||
message = self.message_receiver.recv() => {
|
||||
if message.is_none() {
|
||||
break;
|
||||
}
|
||||
message
|
||||
},
|
||||
message = socket.next() => {
|
||||
let message: RpcMessage = match message {
|
||||
None => break,
|
||||
Some(Ok(message)) => serde_json::from_str(&message)?,
|
||||
Some(Err(LinesCodecError::Io(e))) => Err(e)?,
|
||||
Some(Err(LinesCodecError::MaxLineLengthExceeded)) => unreachable!(),
|
||||
};
|
||||
self.handle_message(message).await?
|
||||
},
|
||||
};
|
||||
if let Some(message) = to_send {
|
||||
let json = serde_json::to_string(&message)?;
|
||||
match socket.send(json).await {
|
||||
Ok(()) => {}
|
||||
Err(LinesCodecError::Io(e)) => Err(e)?,
|
||||
Err(LinesCodecError::MaxLineLengthExceeded) => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_message(
|
||||
&self,
|
||||
message: RpcMessage,
|
||||
) -> Result<Option<RpcMessage>> {
|
||||
if let RpcMessageBody::Call { method, args } = message.body {
|
||||
let response = match self.handlers.get(method.as_str()) {
|
||||
Some(handler) => match handler(args).await {
|
||||
Ok(result) => RpcMessageBody::Respond { response: result },
|
||||
Err(e) => RpcMessageBody::Error {
|
||||
error: e.to_string(),
|
||||
},
|
||||
},
|
||||
None => RpcMessageBody::Error {
|
||||
error: format!("Unknown theseus RPC method {method}"),
|
||||
},
|
||||
};
|
||||
Ok(Some(RpcMessage {
|
||||
id: message.id,
|
||||
body: response,
|
||||
}))
|
||||
} else if let Some(sender) =
|
||||
self.waiting_responses.lock().unwrap().remove(&message.id)
|
||||
{
|
||||
let _ = sender.send(match message.body {
|
||||
RpcMessageBody::Respond { response } => Ok(response),
|
||||
RpcMessageBody::Error { error } => {
|
||||
Err(ErrorKind::RpcError(error).into())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
});
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct RpcMessage {
|
||||
id: Uuid,
|
||||
#[serde(flatten)]
|
||||
body: RpcMessageBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RpcMessageBody {
|
||||
Call {
|
||||
method: String,
|
||||
args: Vec<Value>,
|
||||
},
|
||||
Respond {
|
||||
#[serde(default, skip_serializing_if = "Value::is_null")]
|
||||
response: Value,
|
||||
},
|
||||
Error {
|
||||
error: String,
|
||||
},
|
||||
}
|
||||