From 19da9b7d893381318091dce467c61005d66c7e55 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 15:36:19 +0000 Subject: [PATCH 1/7] Improve ping impl to bring parity to app lib impl --- apps/labrinth/src/models/exp/minecraft.rs | 2 +- apps/labrinth/src/queue/server_ping.rs | 8 ++--- packages/app-lib/src/api/worlds.rs | 34 +++++++-------------- packages/async-minecraft-ping/Cargo.toml | 4 +-- packages/async-minecraft-ping/src/lib.rs | 24 +++++++++++++-- packages/async-minecraft-ping/src/server.rs | 24 +-------------- 6 files changed, 39 insertions(+), 57 deletions(-) diff --git a/apps/labrinth/src/models/exp/minecraft.rs b/apps/labrinth/src/models/exp/minecraft.rs index 1b3c9d9990..225b807c2e 100644 --- a/apps/labrinth/src/models/exp/minecraft.rs +++ b/apps/labrinth/src/models/exp/minecraft.rs @@ -389,7 +389,7 @@ pub struct JavaServerPingData { /// Reported version protocol number of the server. pub version_protocol: i32, /// Description/MOTD of the server as shown in the server list. - pub description: String, + pub description: serde_json::Value, /// Number of players online at the time. pub players_online: i32, /// Maximum number of players allowed on the server. diff --git a/apps/labrinth/src/queue/server_ping.rs b/apps/labrinth/src/queue/server_ping.rs index c65ceb80ba..ac5d24444b 100644 --- a/apps/labrinth/src/queue/server_ping.rs +++ b/apps/labrinth/src/queue/server_ping.rs @@ -6,7 +6,6 @@ use crate::models::exp; use crate::models::ids::ProjectId; use crate::models::projects::ProjectStatus; use crate::{database::PgPool, util::error::Context}; -use async_minecraft_ping::ServerDescription; use chrono::{TimeDelta, Utc}; use clickhouse::{Client, Row}; use serde::Serialize; @@ -282,10 +281,7 @@ pub async fn ping_server( latency: start.elapsed(), version_name: status.version.name, version_protocol: status.version.protocol, - description: match status.description { - ServerDescription::Plain(text) - | ServerDescription::Object { text } => text, - }, + description: status.description, players_online: status.players.online, players_max: status.players.max, }) @@ -303,7 +299,7 @@ struct ServerPingRecord { project_id: u64, address: String, latency_ms: Option, - description: Option, + description: Option, version_name: Option, version_protocol: Option, players_online: Option, diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index 16b87965b7..74749dd6a9 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -13,7 +13,6 @@ pub use crate::util::server_ping::{ }; use crate::util::{io, server_ping}; use crate::{Error, ErrorKind, Result, State, launcher}; -use async_minecraft_ping::ServerDescription; use async_walkdir::WalkDir; use async_zip::{Compression, ZipEntryBuilder}; use chrono::{DateTime, Local, TimeZone, Utc}; @@ -910,8 +909,8 @@ pub async fn get_server_status( "Pinging {address} with protocol version {protocol_version:?}" ); - get_server_status_old(address, protocol_version).await - // get_server_status_new(address, protocol_version).await + // get_server_status_old(address, protocol_version).await + get_server_status_new(address, protocol_version).await } async fn get_server_status_old( @@ -932,19 +931,12 @@ async fn get_server_status_old( .await } -async fn _get_server_status_new( +async fn get_server_status_new( address: &str, protocol_version: Option, ) -> Result { - let (address, port) = match address.rsplit_once(':') { - Some((addr, port)) => { - let port = port.parse::().map_err(|_err| { - Error::from(ErrorKind::InputError("invalid port number".into())) - })?; - (addr, port) - } - None => (address, 25565), - }; + let (address, port) = async_minecraft_ping::parse_host_and_port(address) + .map_err(|err| Error::from(ErrorKind::InputError(err.to_string())))?; let mut builder = async_minecraft_ping::ConnectionConfig::build(address) .with_port(port) @@ -962,15 +954,11 @@ async fn _get_server_status_new( Error::from(ErrorKind::InputError("failed to get server status".into())) })?; let status = &ping_conn.status; - let description = match &status.description { - ServerDescription::Plain(text) => { - serde_json::value::to_raw_value(&text).ok() - } - ServerDescription::Object { text } => { - // TODO: `text` always seems to be empty? - RawValue::from_string(text.clone()).ok() - } - }; + let description = RawValue::from_string( + serde_json::to_string(&status.description) + .expect("serializing should not fail"), + ) + .expect("converting to `RawValue` should not fail"); let players = ServerPlayers { max: status.players.max, @@ -1007,7 +995,7 @@ async fn _get_server_status_new( }; Ok(ServerStatus { - description, + description: Some(description), players: Some(players), version: Some(version), favicon, diff --git a/packages/async-minecraft-ping/Cargo.toml b/packages/async-minecraft-ping/Cargo.toml index 70a91e25ac..7013f0f44f 100644 --- a/packages/async-minecraft-ping/Cargo.toml +++ b/packages/async-minecraft-ping/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "async-minecraft-ping" version = "0.8.0" -authors = ["Jay Vana "] +# authors = ["Jay Vana "] # deprecated edition = "2021" -license = "MIT OR Apache-2.0" description = "An async Rust client for the Minecraft ServerListPing protocol" readme = "README.md" repository = "https://github.com/jsvana/async-minecraft-ping/" +license = "MIT OR Apache-2.0" keywords = ["mc", "minecraft", "serverlistping"] categories = ["api-bindings", "asynchronous"] diff --git a/packages/async-minecraft-ping/src/lib.rs b/packages/async-minecraft-ping/src/lib.rs index 5cd0650a32..57d8c04dc6 100644 --- a/packages/async-minecraft-ping/src/lib.rs +++ b/packages/async-minecraft-ping/src/lib.rs @@ -1,6 +1,26 @@ mod protocol; mod server; +use std::num::ParseIntError; + pub use server::{ - connect, ConnectionConfig, ServerDescription, ServerError, ServerPlayer, ServerPlayers, - ServerVersion, StatusConnection, StatusResponse, + connect, ConnectionConfig, ServerError, ServerPlayer, ServerPlayers, ServerVersion, + StatusConnection, StatusResponse, }; + +pub const DEFAULT_PORT: u16 = 25565; + +#[derive(Debug, thiserror::Error)] +pub enum ParseAddressError { + #[error("failed to parse port")] + ParsePort(#[source] ParseIntError), +} + +pub fn parse_host_and_port(addr: &str) -> Result<(&str, u16), ParseAddressError> { + match addr.rsplit_once(':') { + Some((addr, port)) => { + let port = port.parse::().map_err(ParseAddressError::ParsePort)?; + Ok((addr, port)) + } + None => Ok((addr, DEFAULT_PORT)), + } +} diff --git a/packages/async-minecraft-ping/src/server.rs b/packages/async-minecraft-ping/src/server.rs index 03d3f184b4..fc325f241a 100644 --- a/packages/async-minecraft-ping/src/server.rs +++ b/packages/async-minecraft-ping/src/server.rs @@ -69,14 +69,6 @@ pub struct ServerPlayers { pub sample: Option>, } -/// Contains the server's MOTD. -#[derive(Debug, Deserialize)] -#[serde(untagged)] -pub enum ServerDescription { - Plain(String), - Object { text: String }, -} - /// The decoded JSON response from a status query over /// ServerListPing. #[derive(Debug, Deserialize)] @@ -88,7 +80,7 @@ pub struct StatusResponse { pub players: ServerPlayers, /// Single-field struct containing the server's MOTD. - pub description: ServerDescription, + pub description: serde_json::Value, /// Optional field containing a path to the server's /// favicon. @@ -325,20 +317,6 @@ impl PingConnection { mod tests { use super::*; - #[test] - fn test_server_description_plain() { - let json = r#""A Minecraft Server""#; - let desc: ServerDescription = serde_json::from_str(json).unwrap(); - assert!(matches!(desc, ServerDescription::Plain(s) if s == "A Minecraft Server")); - } - - #[test] - fn test_server_description_object() { - let json = r#"{"text":"A Minecraft Server"}"#; - let desc: ServerDescription = serde_json::from_str(json).unwrap(); - assert!(matches!(desc, ServerDescription::Object { text } if text == "A Minecraft Server")); - } - #[test] fn test_status_response_minimal() { let json = r#"{ From 0856070d268399db194933d56eae3c629172521f Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 16:22:28 +0000 Subject: [PATCH 2/7] Fix issue with new impl --- Cargo.lock | 1 + apps/labrinth/src/models/exp/minecraft.rs | 2 +- apps/labrinth/src/queue/server_ping.rs | 54 ++++++-------- packages/app-lib/Cargo.toml | 1 + packages/app-lib/src/api/worlds.rs | 74 ++++++++++--------- packages/app-lib/src/error.rs | 45 ++++++++++- packages/app-lib/src/util/io.rs | 2 +- .../{server_ping.rs => server_ping/imp.rs} | 50 ------------- packages/app-lib/src/util/server_ping/mod.rs | 43 +++++++++++ packages/async-minecraft-ping/src/server.rs | 12 +-- 10 files changed, 156 insertions(+), 128 deletions(-) rename packages/app-lib/src/util/{server_ping.rs => server_ping/imp.rs} (87%) create mode 100644 packages/app-lib/src/util/server_ping/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 07534c0543..62f6e15cf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10164,6 +10164,7 @@ dependencies = [ "either", "encoding_rs", "enumset", + "eyre", "flate2", "fs4", "futures", diff --git a/apps/labrinth/src/models/exp/minecraft.rs b/apps/labrinth/src/models/exp/minecraft.rs index 225b807c2e..fa330ff4f4 100644 --- a/apps/labrinth/src/models/exp/minecraft.rs +++ b/apps/labrinth/src/models/exp/minecraft.rs @@ -389,7 +389,7 @@ pub struct JavaServerPingData { /// Reported version protocol number of the server. pub version_protocol: i32, /// Description/MOTD of the server as shown in the server list. - pub description: serde_json::Value, + pub description: Option, /// Number of players online at the time. pub players_online: i32, /// Maximum number of players allowed on the server. diff --git a/apps/labrinth/src/queue/server_ping.rs b/apps/labrinth/src/queue/server_ping.rs index ac5d24444b..0df2eb78e8 100644 --- a/apps/labrinth/src/queue/server_ping.rs +++ b/apps/labrinth/src/queue/server_ping.rs @@ -255,42 +255,30 @@ pub async fn ping_server( .map(|duration| duration.min(default_duration)) .unwrap_or(default_duration); - let (address, port) = match address.rsplit_once(':') { - Some((addr, port)) => { - let port = port.parse::().wrap_err("invalid port number")?; - (addr, port) - } - None => (address, 25565), - }; + let (address, port) = async_minecraft_ping::parse_host_and_port(address)?; - let task = async move { - let conn = async_minecraft_ping::ConnectionConfig::build(address) - .with_port(port) - .with_srv_lookup() - .connect() - .await - .wrap_err("failed to connect to server")?; + let conn = async_minecraft_ping::ConnectionConfig::build(address) + .with_port(port) + .with_srv_lookup() + .with_timeout(timeout) + .connect() + .await + .wrap_err("failed to connect to server")?; - let status = conn - .status() - .await - .wrap_err("failed to get server status")? - .status; - - eyre::Ok(exp::minecraft::JavaServerPingData { - latency: start.elapsed(), - version_name: status.version.name, - version_protocol: status.version.protocol, - description: status.description, - players_online: status.players.online, - players_max: status.players.max, - }) - }; - - tokio::time::timeout(timeout, task) + let status = conn + .status() .await - .map_err(eyre::Error::new) - .flatten() + .wrap_err("failed to get server status")? + .status; + + eyre::Ok(exp::minecraft::JavaServerPingData { + latency: start.elapsed(), + version_name: status.version.name, + version_protocol: status.version.protocol, + description: status.description, + players_online: status.players.online, + players_max: status.players.max, + }) } #[derive(Debug, Row, Serialize, Clone)] diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index d2470da7da..39f2ae1a47 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -37,6 +37,7 @@ dunce = { workspace = true } either = { workspace = true } encoding_rs = { workspace = true } enumset = { workspace = true } +eyre = { workspace = true } flate2 = { workspace = true } fs4 = { workspace = true, features = ["tokio"] } futures = { workspace = true, features = ["alloc", "async-await"] } diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index 74749dd6a9..4895d6dba0 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -6,13 +6,13 @@ use crate::state::attached_world_data::AttachedWorldData; use crate::state::{ Profile, ProfileInstallStage, attached_world_data, server_join_log, }; +use crate::util::io; use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS; pub use crate::util::protocol_version::ProtocolVersion; -pub use crate::util::server_ping::{ +use crate::util::server_ping::{ ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion, }; -use crate::util::{io, server_ping}; -use crate::{Error, ErrorKind, Result, State, launcher}; +use crate::{Context, ErrorKind, Result, State, launcher}; use async_walkdir::WalkDir; use async_zip::{Compression, ZipEntryBuilder}; use chrono::{DateTime, Local, TimeZone, Utc}; @@ -913,22 +913,22 @@ pub async fn get_server_status( get_server_status_new(address, protocol_version).await } -async fn get_server_status_old( - address: &str, - protocol_version: Option, -) -> Result { - let (original_host, original_port) = parse_server_address(address)?; - let (host, port) = - resolve_server_address(original_host, original_port).await?; - tracing::debug!( - "Pinging {address} with protocol version {protocol_version:?}" - ); - server_ping::get_server_status( - &(&host as &str, port), - (original_host, original_port), - protocol_version, - ) - .await +// async fn _get_server_status_old( +// address: &str, +// protocol_version: Option, +// ) -> Result { +// let (original_host, original_port) = parse_server_address(address)?; +// let (host, port) = +// resolve_server_address(original_host, original_port).await?; +// tracing::debug!( +// "Pinging {address} with protocol version {protocol_version:?}" +// ); +// server_ping::get_server_status( +// &(&host as &str, port), +// (original_host, original_port), +// protocol_version, +// ) +// .await } async fn get_server_status_new( @@ -936,7 +936,7 @@ async fn get_server_status_new( protocol_version: Option, ) -> Result { let (address, port) = async_minecraft_ping::parse_host_and_port(address) - .map_err(|err| Error::from(ErrorKind::InputError(err.to_string())))?; + .wrap_err("failed to parse address")?; let mut builder = async_minecraft_ping::ConnectionConfig::build(address) .with_port(port) @@ -946,19 +946,22 @@ async fn get_server_status_new( builder = builder.with_protocol_version(version.version as usize) } - let conn = builder.connect().await.map_err(|_err| { - Error::from(ErrorKind::InputError("failed to connect to server".into())) - })?; + let conn = builder + .connect() + .await + .wrap_err("failed to connect to server")?; - let ping_conn = conn.status().await.map_err(|_err| { - Error::from(ErrorKind::InputError("failed to get server status".into())) - })?; + let ping_conn = conn + .status() + .await + .wrap_err("failed to get server status")?; let status = &ping_conn.status; - let description = RawValue::from_string( - serde_json::to_string(&status.description) - .expect("serializing should not fail"), - ) - .expect("converting to `RawValue` should not fail"); + let description = status.description.as_ref().map(|d| { + let json = + serde_json::to_string(d).expect("serializing should not fail"); + RawValue::from_string(json) + .expect("converting to `RawValue` should not fail") + }); let players = ServerPlayers { max: status.players.max, @@ -988,14 +991,15 @@ async fn get_server_status_new( let latency = { let start = Instant::now(); let ping_magic = Utc::now().timestamp_millis().cast_unsigned(); - ping_conn.ping(ping_magic).await.map_err(|_err| { - Error::from(ErrorKind::InputError("failed to do ping".into())) - })?; + ping_conn + .ping(ping_magic) + .await + .wrap_err("failed to do ping")?; start.elapsed().as_millis() as i64 }; Ok(ServerStatus { - description: Some(description), + description, players: Some(players), version: Some(version), favicon, diff --git a/packages/app-lib/src/error.rs b/packages/app-lib/src/error.rs index f18bf3a44a..0b734c5d03 100644 --- a/packages/app-lib/src/error.rs +++ b/packages/app-lib/src/error.rs @@ -1,5 +1,9 @@ //! Theseus error type -use std::sync::Arc; +use std::{ + convert::Infallible, + fmt::{Debug, Display}, + sync::Arc, +}; use crate::{profile, util}; use data_url::DataUrlError; @@ -220,4 +224,41 @@ impl ErrorKind { } } -pub type Result = core::result::Result; +pub type Result = core::result::Result; + +pub trait Context: Sized { + fn wrap_err_with(self, f: impl FnOnce() -> D) -> Result + where + D: Send + Sync + Debug + Display + 'static; + + #[inline] + fn wrap_err(self, msg: D) -> Result + where + D: Send + Sync + Debug + Display + 'static, + { + self.wrap_err_with(|| msg) + } +} + +impl Context for Result +where + Self: eyre::WrapErr, +{ + fn wrap_err_with(self, f: impl FnOnce() -> D) -> Result + where + D: Send + Sync + Debug + Display + 'static, + { + eyre::WrapErr::wrap_err_with(self, f).map_err(|err| { + Error::from(ErrorKind::OtherError(format!("{err:#}"))) + }) + } +} + +impl Context for Option { + fn wrap_err_with(self, f: impl FnOnce() -> D) -> Result + where + D: Send + Sync + Debug + Display + 'static, + { + self.ok_or_else(|| Error::from(ErrorKind::OtherError(f().to_string()))) + } +} diff --git a/packages/app-lib/src/util/io.rs b/packages/app-lib/src/util/io.rs index 7bdc358f68..4ec3b39758 100644 --- a/packages/app-lib/src/util/io.rs +++ b/packages/app-lib/src/util/io.rs @@ -318,7 +318,7 @@ macro_rules! get_resource_file { Ok(dir) => dir, Err(e) => { break 'get_resource_file $crate::Result::Err( - $crate::util::io::IOError::from(e).into(), + $crate::Error::from($crate::util::io::IOError::from(e)), ); } }; diff --git a/packages/app-lib/src/util/server_ping.rs b/packages/app-lib/src/util/server_ping/imp.rs similarity index 87% rename from packages/app-lib/src/util/server_ping.rs rename to packages/app-lib/src/util/server_ping/imp.rs index d03991fbca..e7a0b33fc6 100644 --- a/packages/app-lib/src/util/server_ping.rs +++ b/packages/app-lib/src/util/server_ping/imp.rs @@ -1,53 +1,3 @@ -use crate::ErrorKind; -use crate::error::Result; -use crate::util::protocol_version::ProtocolVersion; -use serde::{Deserialize, Serialize}; -use serde_json::value::RawValue; -use std::time::Duration; -use tokio::net::ToSocketAddrs; -use tokio::select; -use url::Url; - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ServerStatus { - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub players: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub favicon: Option, - #[serde(default)] - pub enforces_secure_chat: bool, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ping: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ServerPlayers { - pub max: i32, - pub online: i32, - #[serde(default)] - pub sample: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ServerGameProfile { - pub id: String, - pub name: String, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ServerVersion { - pub name: String, - pub protocol: i32, - #[serde(skip_deserializing)] - pub legacy: bool, -} - pub async fn get_server_status( address: &impl ToSocketAddrs, original_address: (&str, u16), diff --git a/packages/app-lib/src/util/server_ping/mod.rs b/packages/app-lib/src/util/server_ping/mod.rs new file mode 100644 index 0000000000..1254d368e7 --- /dev/null +++ b/packages/app-lib/src/util/server_ping/mod.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use url::Url; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ServerStatus { + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub players: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub favicon: Option, + #[serde(default)] + pub enforces_secure_chat: bool, + + #[serde(skip_serializing_if = "Option::is_none")] + pub ping: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerPlayers { + pub max: i32, + pub online: i32, + #[serde(default)] + pub sample: Vec, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerGameProfile { + pub id: String, + pub name: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ServerVersion { + pub name: String, + pub protocol: i32, + #[serde(skip_deserializing)] + pub legacy: bool, +} diff --git a/packages/async-minecraft-ping/src/server.rs b/packages/async-minecraft-ping/src/server.rs index fc325f241a..ceaf866ca6 100644 --- a/packages/async-minecraft-ping/src/server.rs +++ b/packages/async-minecraft-ping/src/server.rs @@ -3,7 +3,7 @@ use std::time::Duration; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::net::TcpStream; @@ -34,7 +34,7 @@ impl From for ServerError { } /// Contains information about the server version. -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ServerVersion { /// The server's Minecraft version, i.e. "1.15.2". pub name: String, @@ -44,7 +44,7 @@ pub struct ServerVersion { } /// Contains information about a player. -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ServerPlayer { /// The player's in-game name. pub name: String, @@ -55,7 +55,7 @@ pub struct ServerPlayer { /// Contains information about the currently online /// players. -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct ServerPlayers { /// The configured maximum number of players for the /// server. @@ -71,7 +71,7 @@ pub struct ServerPlayers { /// The decoded JSON response from a status query over /// ServerListPing. -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct StatusResponse { /// Information about the server's version. pub version: ServerVersion, @@ -80,7 +80,7 @@ pub struct StatusResponse { pub players: ServerPlayers, /// Single-field struct containing the server's MOTD. - pub description: serde_json::Value, + pub description: Option, /// Optional field containing a path to the server's /// favicon. From ba6ac40574794328f4d4fde993f584c88d07fce7 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 16:45:40 +0000 Subject: [PATCH 3/7] fix labrinth compile --- apps/labrinth/src/lib.rs | 2 ++ apps/labrinth/src/queue/server_ping.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 46ac812904..82303656a9 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -1,3 +1,5 @@ +#![recursion_limit = "256"] + use std::sync::Arc; use std::time::Duration; diff --git a/apps/labrinth/src/queue/server_ping.rs b/apps/labrinth/src/queue/server_ping.rs index 0df2eb78e8..952f5407b8 100644 --- a/apps/labrinth/src/queue/server_ping.rs +++ b/apps/labrinth/src/queue/server_ping.rs @@ -106,7 +106,12 @@ impl ServerPingQueue { project_id: project_id.0, address: ping.address.clone(), latency_ms: data.map(|d| d.latency.as_millis() as u32), - description: data.map(|d| d.description.clone()), + description: data.and_then(|d| { + d.description.as_ref().map(|d| { + serde_json::to_string(&d) + .expect("serialization should not fail") + }) + }), version_name: data.map(|d| d.version_name.clone()), version_protocol: data.map(|d| d.version_protocol), players_online: data.map(|d| d.players_online), @@ -287,7 +292,7 @@ struct ServerPingRecord { project_id: u64, address: String, latency_ms: Option, - description: Option, + description: Option, version_name: Option, version_protocol: Option, players_online: Option, From 2c10a2afcca3de0aac449f896acfa9bbf26f85fc Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 16:56:16 +0000 Subject: [PATCH 4/7] wip: why do servers not provide server info.. --- apps/labrinth/src/models/exp/minecraft.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/labrinth/src/models/exp/minecraft.rs b/apps/labrinth/src/models/exp/minecraft.rs index fa330ff4f4..d0ccb105a4 100644 --- a/apps/labrinth/src/models/exp/minecraft.rs +++ b/apps/labrinth/src/models/exp/minecraft.rs @@ -391,9 +391,9 @@ pub struct JavaServerPingData { /// Description/MOTD of the server as shown in the server list. pub description: Option, /// Number of players online at the time. - pub players_online: i32, + pub players_online: Option, /// Maximum number of players allowed on the server. - pub players_max: i32, + pub players_max: Option, } component::relations! { From e6ef671d0cd009257448ecac41058590bcd8f8a1 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 17:31:47 +0000 Subject: [PATCH 5/7] Fix ping impl overriding port --- apps/labrinth/src/queue/server_ping.rs | 7 +- packages/app-lib/src/api/worlds.rs | 5 - packages/app-lib/src/util/server_ping/imp.rs | 4 +- packages/app-lib/src/util/server_ping/mod.rs | 4 +- .../async-minecraft-ping/examples/status.rs | 10 +- packages/async-minecraft-ping/src/lib.rs | 20 ---- packages/async-minecraft-ping/src/server.rs | 104 ++++++++++++------ .../async-minecraft-ping/tests/integration.rs | 6 +- 8 files changed, 83 insertions(+), 77 deletions(-) diff --git a/apps/labrinth/src/queue/server_ping.rs b/apps/labrinth/src/queue/server_ping.rs index 952f5407b8..d3f6d53153 100644 --- a/apps/labrinth/src/queue/server_ping.rs +++ b/apps/labrinth/src/queue/server_ping.rs @@ -114,8 +114,8 @@ impl ServerPingQueue { }), version_name: data.map(|d| d.version_name.clone()), version_protocol: data.map(|d| d.version_protocol), - players_online: data.map(|d| d.players_online), - players_max: data.map(|d| d.players_max), + players_online: data.and_then(|d| d.players_online), + players_max: data.and_then(|d| d.players_max), }; ch.write(&row) @@ -260,10 +260,7 @@ pub async fn ping_server( .map(|duration| duration.min(default_duration)) .unwrap_or(default_duration); - let (address, port) = async_minecraft_ping::parse_host_and_port(address)?; - let conn = async_minecraft_ping::ConnectionConfig::build(address) - .with_port(port) .with_srv_lookup() .with_timeout(timeout) .connect() diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index 4895d6dba0..de92951517 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -929,17 +929,12 @@ pub async fn get_server_status( // protocol_version, // ) // .await -} async fn get_server_status_new( address: &str, protocol_version: Option, ) -> Result { - let (address, port) = async_minecraft_ping::parse_host_and_port(address) - .wrap_err("failed to parse address")?; - let mut builder = async_minecraft_ping::ConnectionConfig::build(address) - .with_port(port) .with_srv_lookup(); if let Some(version) = protocol_version { diff --git a/packages/app-lib/src/util/server_ping/imp.rs b/packages/app-lib/src/util/server_ping/imp.rs index e7a0b33fc6..6696d8a079 100644 --- a/packages/app-lib/src/util/server_ping/imp.rs +++ b/packages/app-lib/src/util/server_ping/imp.rs @@ -255,8 +255,8 @@ mod legacy { }), description: parts.next().and_then(|x| to_raw_value(x).ok()), players: Some(ServerPlayers { - online: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1), - max: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1), + online: parts.next().and_then(|x| x.parse().ok()), + max: parts.next().and_then(|x| x.parse().ok()), sample: vec![], }), favicon: None, diff --git a/packages/app-lib/src/util/server_ping/mod.rs b/packages/app-lib/src/util/server_ping/mod.rs index 1254d368e7..40ad314da9 100644 --- a/packages/app-lib/src/util/server_ping/mod.rs +++ b/packages/app-lib/src/util/server_ping/mod.rs @@ -22,8 +22,8 @@ pub struct ServerStatus { #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ServerPlayers { - pub max: i32, - pub online: i32, + pub max: Option, + pub online: Option, #[serde(default)] pub sample: Vec, } diff --git a/packages/async-minecraft-ping/examples/status.rs b/packages/async-minecraft-ping/examples/status.rs index 3715926e74..14308f02cc 100644 --- a/packages/async-minecraft-ping/examples/status.rs +++ b/packages/async-minecraft-ping/examples/status.rs @@ -37,10 +37,12 @@ async fn main() -> Result<()> { let connection = connection.status().await?; - println!( - "{} of {} player(s) online", - connection.status.players.online, connection.status.players.max - ); + if let (Some(online), Some(max)) = ( + connection.status.players.online, + connection.status.players.max, + ) { + println!("{online} of {max} player(s) online"); + } connection.ping(42).await?; diff --git a/packages/async-minecraft-ping/src/lib.rs b/packages/async-minecraft-ping/src/lib.rs index 57d8c04dc6..4fc6adc00e 100644 --- a/packages/async-minecraft-ping/src/lib.rs +++ b/packages/async-minecraft-ping/src/lib.rs @@ -1,26 +1,6 @@ mod protocol; mod server; -use std::num::ParseIntError; - pub use server::{ connect, ConnectionConfig, ServerError, ServerPlayer, ServerPlayers, ServerVersion, StatusConnection, StatusResponse, }; - -pub const DEFAULT_PORT: u16 = 25565; - -#[derive(Debug, thiserror::Error)] -pub enum ParseAddressError { - #[error("failed to parse port")] - ParsePort(#[source] ParseIntError), -} - -pub fn parse_host_and_port(addr: &str) -> Result<(&str, u16), ParseAddressError> { - match addr.rsplit_once(':') { - Some((addr, port)) => { - let port = port.parse::().map_err(ParseAddressError::ParsePort)?; - Ok((addr, port)) - } - None => Ok((addr, DEFAULT_PORT)), - } -} diff --git a/packages/async-minecraft-ping/src/server.rs b/packages/async-minecraft-ping/src/server.rs index ceaf866ca6..8765b5b7a5 100644 --- a/packages/async-minecraft-ping/src/server.rs +++ b/packages/async-minecraft-ping/src/server.rs @@ -55,14 +55,14 @@ pub struct ServerPlayer { /// Contains information about the currently online /// players. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize)] pub struct ServerPlayers { /// The configured maximum number of players for the /// server. - pub max: i32, + pub max: Option, /// The number of players currently online. - pub online: i32, + pub online: Option, /// An optional list of player information for /// currently online players. @@ -77,6 +77,7 @@ pub struct StatusResponse { pub version: ServerVersion, /// Information about currently online players. + #[serde(default)] pub players: ServerPlayers, /// Single-field struct containing the server's MOTD. @@ -96,7 +97,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2); pub struct ConnectionConfig { protocol_version: usize, address: String, - port: u16, + port_override: Option, timeout: Duration, #[cfg(feature = "srv")] srv_lookup: bool, @@ -105,11 +106,19 @@ pub struct ConnectionConfig { impl ConnectionConfig { /// Initiates the Minecraft server /// connection build process. - pub fn build>(address: T) -> Self { + pub fn build(address: impl AsRef) -> Self { + let (address, port_override) = match address.as_ref().rsplit_once(':') { + Some((addr, port)) => match port.parse::() { + Ok(port) => (addr, Some(port)), + Err(_) => (addr, None), + }, + None => (address.as_ref(), None), + }; + ConnectionConfig { protocol_version: LATEST_PROTOCOL_VERSION, - address: address.into(), - port: DEFAULT_PORT, + address: address.to_string(), + port_override, timeout: DEFAULT_TIMEOUT, #[cfg(feature = "srv")] srv_lookup: false, @@ -129,7 +138,7 @@ impl ConnectionConfig { /// connection to use. If not specified, the /// default port of 25565 will be used. pub fn with_port(mut self, port: u16) -> Self { - self.port = port; + self.port_override = Some(port); self } @@ -157,7 +166,8 @@ impl ConnectionConfig { /// Connects to the server and consumes the builder. pub async fn connect(self) -> Result { - let (address, port) = self.resolve_address().await; + let (address, resolved_port) = self.resolve_address().await; + let port = self.port_override.or(resolved_port).unwrap_or(DEFAULT_PORT); let stream = tokio::time::timeout( self.timeout, @@ -177,43 +187,43 @@ impl ConnectionConfig { } #[cfg(feature = "srv")] - async fn resolve_address(&self) -> (String, u16) { + async fn resolve_address(&self) -> (String, Option) { if !self.srv_lookup { - return (self.address.clone(), self.port); + return (self.address.clone(), None); } // Try to resolve SRV record, fall back to original address on any failure - match self.lookup_srv().await { - Some((host, port)) => (host, port), - None => (self.address.clone(), self.port), + match lookup_srv(&self.address, self.timeout).await { + Some((host, port)) => (host, Some(port)), + None => (self.address.clone(), None), } } #[cfg(not(feature = "srv"))] - async fn resolve_address(&self) -> (String, u16) { - (self.address.clone(), self.port) + async fn resolve_address(&self) -> (String, Option) { + (self.address.clone(), None) } +} - #[cfg(feature = "srv")] - async fn lookup_srv(&self) -> Option<(String, u16)> { - use hickory_resolver::TokioAsyncResolver; +#[cfg(feature = "srv")] +async fn lookup_srv(address: &str, timeout: Duration) -> Option<(String, u16)> { + use hickory_resolver::TokioAsyncResolver; - let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?; - let srv_name = format!("_minecraft._tcp.{}", self.address); + let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?; + let srv_name = format!("_minecraft._tcp.{address}"); - let lookup = tokio::time::timeout(self.timeout, resolver.srv_lookup(&srv_name)) - .await - .ok()? - .ok()?; + let lookup = tokio::time::timeout(timeout, resolver.srv_lookup(&srv_name)) + .await + .ok()? + .ok()?; - let record = lookup.iter().next()?; - let target = record.target().to_string(); - // Remove trailing dot from DNS name - let host = target.trim_end_matches('.').to_string(); - let port = record.port(); + let record = lookup.iter().next()?; + let target = record.target().to_string(); + // Remove trailing dot from DNS name + let host = target.trim_end_matches('.').to_string(); + let port = record.port(); - Some((host, port)) - } + Some((host, port)) } /// Convenience wrapper for easily connecting @@ -319,6 +329,22 @@ mod tests { #[test] fn test_status_response_minimal() { + let json = r#"{ + "version": {"name": "1.20.4", "protocol": 765} + }"#; + + let response: StatusResponse = serde_json::from_str(json).unwrap(); + assert_eq!(response.version.name, "1.20.4"); + assert_eq!(response.version.protocol, 765); + assert_eq!(response.players.max, None); + assert_eq!(response.players.online, None); + assert!(response.description.is_none()); + assert!(response.players.sample.is_none()); + assert!(response.favicon.is_none()); + } + + #[test] + fn test_status_response_small() { let json = r#"{ "version": {"name": "1.20.4", "protocol": 765}, "players": {"max": 20, "online": 5}, @@ -328,8 +354,8 @@ mod tests { let response: StatusResponse = serde_json::from_str(json).unwrap(); assert_eq!(response.version.name, "1.20.4"); assert_eq!(response.version.protocol, 765); - assert_eq!(response.players.max, 20); - assert_eq!(response.players.online, 5); + assert_eq!(response.players.max, Some(20)); + assert_eq!(response.players.online, Some(5)); assert!(response.players.sample.is_none()); assert!(response.favicon.is_none()); } @@ -374,15 +400,21 @@ mod tests { fn test_connection_config_defaults() { let config = ConnectionConfig::build("localhost"); assert_eq!(config.address, "localhost"); - assert_eq!(config.port, DEFAULT_PORT); + assert_eq!(config.port_override, None); assert_eq!(config.timeout, DEFAULT_TIMEOUT); assert_eq!(config.protocol_version, LATEST_PROTOCOL_VERSION); } + #[test] + fn test_connection_config_with_port_in_address() { + let config = ConnectionConfig::build("localhost:12345"); + assert_eq!(config.port_override, Some(12345)); + } + #[test] fn test_connection_config_with_port() { let config = ConnectionConfig::build("localhost").with_port(12345); - assert_eq!(config.port, 12345); + assert_eq!(config.port_override, Some(12345)); } #[test] diff --git a/packages/async-minecraft-ping/tests/integration.rs b/packages/async-minecraft-ping/tests/integration.rs index 58d81e950b..8ee7d38b80 100644 --- a/packages/async-minecraft-ping/tests/integration.rs +++ b/packages/async-minecraft-ping/tests/integration.rs @@ -250,8 +250,8 @@ async fn test_status_json_parsing_plain_description() { let response: StatusResponse = serde_json::from_str(json).unwrap(); assert_eq!(response.version.name, "1.20.4"); assert_eq!(response.version.protocol, 765); - assert_eq!(response.players.max, 100); - assert_eq!(response.players.online, 42); + assert_eq!(response.players.max, Some(100)); + assert_eq!(response.players.online, Some(42)); } #[tokio::test] @@ -267,7 +267,7 @@ async fn test_status_json_parsing_object_description() { let response: StatusResponse = serde_json::from_str(json).unwrap(); assert_eq!(response.version.name, "1.19.4"); - assert_eq!(response.players.online, 10); + assert_eq!(response.players.online, Some(10)); assert!(response.players.sample.is_some()); assert_eq!(response.players.sample.as_ref().unwrap().len(), 1); assert_eq!(response.players.sample.as_ref().unwrap()[0].name, "Notch"); From ee03399fc63dc4ef3f82f4102b8082b306599b07 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 17:46:19 +0000 Subject: [PATCH 6/7] fix theseus_gui --- packages/app-lib/src/api/worlds.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-lib/src/api/worlds.rs b/packages/app-lib/src/api/worlds.rs index de92951517..4cdd060889 100644 --- a/packages/app-lib/src/api/worlds.rs +++ b/packages/app-lib/src/api/worlds.rs @@ -9,7 +9,7 @@ use crate::state::{ use crate::util::io; use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS; pub use crate::util::protocol_version::ProtocolVersion; -use crate::util::server_ping::{ +pub use crate::util::server_ping::{ ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion, }; use crate::{Context, ErrorKind, Result, State, launcher}; From 2781270417be70b2b268024d436e5104ed4bd786 Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 9 Mar 2026 23:22:14 +0000 Subject: [PATCH 7/7] remove unneeded recursion lmit --- apps/labrinth/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index 82303656a9..46ac812904 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -1,5 +1,3 @@ -#![recursion_limit = "256"] - use std::sync::Arc; use std::time::Duration;