Files
MetaCrate/crates/libremetaverse/src/client_core.rs
Chili Palmer c9a1170a27
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Complete first release candidate audit (#107)
2026-08-12 14:44:28 +00:00

1976 lines
67 KiB
Rust

//! Runtime-neutral client configuration, composition, and lifecycle ownership.
use crate::{
AgentSettings, AssetCacheSettings, ConnectionSettings, LoggingSettings, PacketSettings,
ParcelSettings, TexturePipelineSettings, TimingSettings, WorldSettings,
};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource, TimeProvider};
use std::fmt;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex};
/// A validated client-core failure with no credential or capability payload.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ClientCoreError {
/// A public settings field failed validation.
InvalidConfiguration {
field: &'static str,
reason: &'static str,
},
/// A lifecycle operation was requested in an incompatible state.
InvalidLifecycle {
operation: &'static str,
state: ClientLifecycleState,
},
/// An owned service could not complete orderly shutdown.
ServiceShutdown { service: &'static str },
}
impl fmt::Display for ClientCoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfiguration { field, reason } => {
write!(formatter, "invalid client setting {field}: {reason}")
}
Self::InvalidLifecycle { operation, state } => {
write!(formatter, "cannot {operation} while client is {state:?}")
}
Self::ServiceShutdown { service } => {
write!(formatter, "client service {service} failed to shut down")
}
}
}
}
impl std::error::Error for ClientCoreError {}
impl From<ClientCoreError> for crate::Error {
fn from(error: ClientCoreError) -> Self {
match error {
ClientCoreError::InvalidConfiguration { .. } => Self::Argument,
ClientCoreError::InvalidLifecycle { .. } | ClientCoreError::ServiceShutdown { .. } => {
Self::InvalidOperation
}
}
}
}
/// Observable lifecycle of a [`GridClient`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum ClientLifecycleState {
Active,
ShuttingDown,
Disposed,
}
impl ClientLifecycleState {
fn from_u8(value: u8) -> Self {
match value {
0 => Self::Active,
1 => Self::ShuttingDown,
_ => Self::Disposed,
}
}
}
/// Defines the C#-compatible shutdown order for explicitly composed services.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum ShutdownPhase {
Network = 0,
Manager = 1,
Http = 2,
RateLimiter = 3,
}
/// A client-owned subsystem that participates in deterministic teardown.
///
/// Implementations own their tasks and I/O resources. They must use the client
/// cancellation token and must not require `GridClient` to create an executor.
pub trait ClientService: Send + Sync {
/// Stable diagnostic name. It must not contain credentials or capability URLs.
fn name(&self) -> &'static str;
fn shutdown_phase(&self) -> ShutdownPhase {
ShutdownPhase::Manager
}
/// Cancels and joins resources owned by this service.
///
/// # Errors
///
/// Returns a redacted service error when orderly teardown cannot complete.
fn shutdown(&self) -> Result<(), ClientCoreError>;
}
struct ServiceRegistration {
insertion_order: usize,
service: Arc<dyn ClientService>,
}
struct ClientRuntime {
state: AtomicU8,
cancellation: CancellationTokenSource,
services: Mutex<Vec<ServiceRegistration>>,
agent_throttle_sender: Mutex<Option<Arc<dyn crate::udp_transport::AgentThrottleSender>>>,
http_caps_client: Mutex<crate::caps_http::HttpCapsClient>,
caps_rate_limiter: Mutex<crate::caps_http::CapsRateLimiter>,
utilization_statistics: Mutex<crate::stats::UtilizationStatistics>,
throttle: Mutex<crate::AgentThrottle>,
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
object_manager: Mutex<Option<Arc<crate::object_manager::ObjectManagerInner>>>,
environment_manager: Mutex<Option<Arc<crate::environment_manager::EnvironmentManagerInner>>>,
estate_tools: Mutex<Option<Arc<crate::estate_tools::EstateToolsInner>>>,
terrain_manager: Mutex<Option<Arc<crate::terrain_manager::TerrainManagerInner>>>,
sound_manager: Mutex<Option<Arc<crate::sound_manager::SoundManagerInner>>>,
parcel_manager: Mutex<Option<Arc<crate::parcel_manager::ParcelManagerInner>>>,
grid_manager: Mutex<Option<Arc<crate::grid_manager::GridManagerInner>>>,
directory_manager: Mutex<Option<Arc<crate::directory_manager::DirectoryManagerInner>>>,
interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>,
friends_manager: Mutex<Option<Arc<crate::friends_manager::FriendsManagerInner>>>,
group_manager: Mutex<Option<Arc<crate::group_manager::GroupManagerInner>>>,
marketplace_manager: Mutex<Option<crate::marketplace_runtime::MarketplaceManager>>,
appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
asset_manager: Mutex<Option<Arc<crate::asset_manager::AssetManagerInner>>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
fn clear_cached<T>(slot: &Mutex<Option<T>>) {
slot.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
impl ClientRuntime {
fn new(
services: Vec<Arc<dyn ClientService>>,
http_caps_client: crate::caps_http::HttpCapsClient,
caps_rate_limiter: crate::caps_http::CapsRateLimiter,
) -> Self {
Self {
state: AtomicU8::new(ClientLifecycleState::Active as u8),
cancellation: CancellationTokenSource::new(),
services: Mutex::new(
services
.into_iter()
.enumerate()
.map(|(insertion_order, service)| ServiceRegistration {
insertion_order,
service,
})
.collect(),
),
agent_throttle_sender: Mutex::new(None),
http_caps_client: Mutex::new(http_caps_client),
caps_rate_limiter: Mutex::new(caps_rate_limiter),
utilization_statistics: Mutex::new(
crate::stats::UtilizationStatistics::new()
.expect("utilization statistics construction is infallible"),
),
throttle: Mutex::new(crate::AgentThrottle::default()),
network_manager: Mutex::new(std::sync::Weak::new()),
agent_manager: Mutex::new(std::sync::Weak::new()),
object_manager: Mutex::new(None),
environment_manager: Mutex::new(None),
estate_tools: Mutex::new(None),
terrain_manager: Mutex::new(None),
sound_manager: Mutex::new(None),
parcel_manager: Mutex::new(None),
grid_manager: Mutex::new(None),
directory_manager: Mutex::new(None),
interest_list_manager: Mutex::new(None),
friends_manager: Mutex::new(None),
group_manager: Mutex::new(None),
marketplace_manager: Mutex::new(None),
appearance_manager: Mutex::new(None),
avatar_manager: Mutex::new(None),
animesh_manager: Mutex::new(None),
inventory_manager: Mutex::new(None),
inventory_ais_client: Mutex::new(None),
asset_manager: Mutex::new(None),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
}
fn state(&self) -> ClientLifecycleState {
ClientLifecycleState::from_u8(self.state.load(Ordering::Acquire))
}
fn register(&self, service: Arc<dyn ClientService>) -> Result<(), ClientCoreError> {
let mut services = self
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let state = self.state();
if state != ClientLifecycleState::Active {
return Err(ClientCoreError::InvalidLifecycle {
operation: "register a service",
state,
});
}
let insertion_order = services.len();
services.push(ServiceRegistration {
insertion_order,
service,
});
Ok(())
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
if self
.state
.compare_exchange(
ClientLifecycleState::Active as u8,
ClientLifecycleState::ShuttingDown as u8,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err()
{
let mut wait = self
.shutdown_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while self.state() == ClientLifecycleState::ShuttingDown {
wait = self
.shutdown_complete
.wait(wait)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
return Ok(());
}
self.cancellation.cancel();
// Service shutdown is user-controlled code. Move registrations out
// before invoking it so re-entrant lifecycle calls cannot deadlock on
// the registry mutex.
let mut services = std::mem::take(
&mut *self
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
services.sort_by_key(|entry| (entry.service.shutdown_phase(), entry.insertion_order));
let mut first_error = None;
for entry in &services {
if let Err(error) = entry.service.shutdown()
&& first_error.is_none()
{
first_error = Some(error);
}
}
drop(services);
clear_cached(&self.agent_throttle_sender);
clear_cached(&self.inventory_manager);
clear_cached(&self.inventory_ais_client);
clear_cached(&self.asset_manager);
clear_cached(&self.appearance_manager);
clear_cached(&self.avatar_manager);
clear_cached(&self.animesh_manager);
clear_cached(&self.object_manager);
clear_cached(&self.environment_manager);
clear_cached(&self.estate_tools);
clear_cached(&self.terrain_manager);
clear_cached(&self.sound_manager);
clear_cached(&self.parcel_manager);
clear_cached(&self.grid_manager);
clear_cached(&self.directory_manager);
clear_cached(&self.interest_list_manager);
clear_cached(&self.friends_manager);
clear_cached(&self.group_manager);
clear_cached(&self.marketplace_manager);
self.http_caps_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.shutdown();
let _ = self
.caps_rate_limiter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.dispose();
let shutdown_wait = self
.shutdown_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.state
.store(ClientLifecycleState::Disposed as u8, Ordering::Release);
self.shutdown_complete.notify_all();
drop(shutdown_wait);
first_error.map_or(Ok(()), Err)
}
}
/// A non-owning route back to a live client runtime.
///
/// Manager caches can hold this handle without creating an `Arc` cycle. The
/// settings and time provider are immutable client composition data and are
/// cloned only when a manager needs to perform an operation.
#[derive(Clone)]
pub(crate) struct ClientWeakHandle {
runtime: std::sync::Weak<ClientRuntime>,
settings: Settings,
time_provider: TimeProvider,
}
impl ClientWeakHandle {
pub(crate) fn upgrade(&self) -> Option<GridClient> {
Some(GridClient {
settings: self.settings.clone(),
time_provider: self.time_provider.clone(),
runtime: self.runtime.upgrade()?,
network_manager: None,
agent_manager: None,
})
}
}
/// Native owner for the mapped C# `GridClient` lifecycle slice.
///
/// Construction is side-effect free: it creates no runtime, task, socket, or
/// HTTP request. Network-facing services are attached explicitly and are owned
/// until ordered shutdown or drop.
pub struct GridClient {
pub(crate) settings: Settings,
pub(crate) time_provider: TimeProvider,
runtime: Arc<ClientRuntime>,
network_manager: Option<crate::NetworkManager>,
agent_manager: Option<Box<crate::agent_manager::AgentManager>>,
}
impl Clone for GridClient {
fn clone(&self) -> Self {
Self {
settings: self.settings.clone(),
time_provider: self.time_provider.clone(),
runtime: Arc::clone(&self.runtime),
network_manager: self.network_manager.clone(),
agent_manager: None,
}
}
}
#[cfg(test)]
pub(crate) struct ClientRetentionProbe(std::sync::Weak<ClientRuntime>);
#[cfg(test)]
impl ClientRetentionProbe {
pub(crate) fn is_released(&self) -> bool {
self.0.upgrade().is_none()
}
}
impl GridClient {
#[must_use]
pub fn builder() -> GridClientBuilder {
GridClientBuilder::default()
}
/// Returns a shared settings view without requiring mutable client access.
#[must_use]
pub fn settings_ref(&self) -> &Settings {
&self.settings
}
#[must_use]
pub fn cancellation_token(&self) -> CancellationToken {
self.runtime.cancellation.token()
}
#[must_use]
pub fn lifecycle_state(&self) -> ClientLifecycleState {
self.runtime.state()
}
/// Adds an explicitly constructed service to this client's ownership tree.
///
/// # Errors
///
/// Registration fails after shutdown begins.
pub fn register_service(
&mut self,
service: Arc<dyn ClientService>,
) -> Result<(), ClientCoreError> {
self.runtime.register(service)
}
/// Installs the network-owned callback used by mapped `AgentThrottle.Set`.
///
/// # Errors
///
/// Registration fails after client shutdown begins.
pub fn set_agent_throttle_sender(
&mut self,
sender: Arc<dyn crate::udp_transport::AgentThrottleSender>,
) -> Result<(), ClientCoreError> {
let mut registered = self
.runtime
.agent_throttle_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let state = self.lifecycle_state();
if state != ClientLifecycleState::Active {
return Err(ClientCoreError::InvalidLifecycle {
operation: "register the agent throttle sender",
state,
});
}
*registered = Some(sender);
Ok(())
}
pub(crate) fn agent_throttle_sender(
&self,
) -> Option<Arc<dyn crate::udp_transport::AgentThrottleSender>> {
self.runtime
.agent_throttle_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn native_http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
self.runtime
.http_caps_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn native_set_http_caps_client(&mut self, value: crate::caps_http::HttpCapsClient) {
*self
.runtime
.http_caps_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
}
pub(crate) fn native_caps_rate_limiter(&self) -> crate::caps_http::CapsRateLimiter {
self.runtime
.caps_rate_limiter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn native_network(&self) -> Result<crate::NetworkManager, crate::Error> {
if let Some(manager) = &self.network_manager {
return Ok(manager.clone());
}
let mut cached = self
.runtime
.network_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.upgrade() {
return Ok(crate::network_manager::NetworkManager::native_from_inner(
inner,
));
}
let manager = crate::network_manager::NetworkManager::native_new(self.clone())?;
*cached = manager.native_inner_weak();
Ok(manager)
}
pub(crate) fn native_weak_handle(&self) -> ClientWeakHandle {
ClientWeakHandle {
runtime: Arc::downgrade(&self.runtime),
settings: self.settings.clone(),
time_provider: self.time_provider.clone(),
}
}
pub(crate) fn native_inventory(&self) -> Result<crate::InventoryManager, crate::Error> {
let mut cached = self
.runtime
.inventory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Ok(
crate::inventory_manager::InventoryManager::native_from_inner(Arc::clone(inner)),
);
}
let manager =
crate::inventory_manager::InventoryManager::native_new(Some(Arc::new(self.clone())))?;
*cached = Some(manager.native_inner());
Ok(manager)
}
pub(crate) fn native_appearance(&self) -> Result<crate::AppearanceManager, crate::Error> {
let mut cached = self
.runtime
.appearance_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Ok(
crate::appearance_manager::AppearanceManager::native_from_inner(Arc::clone(inner)),
);
}
let manager =
crate::appearance_manager::AppearanceManager::native_new(Some(Arc::new(self.clone())))?;
*cached = Some(manager.native_inner());
Ok(manager)
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_appearance(&mut self, value: crate::AppearanceManager) {
*self
.runtime
.appearance_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
pub(crate) fn native_assets(&self) -> Result<crate::AssetManager, crate::Error> {
let mut cached = self
.runtime
.asset_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return crate::asset_manager::AssetManager::native_from_inner(Arc::clone(inner));
}
let manager = crate::asset_manager::AssetManager::new(Some(self.clone()))?;
*cached = Some(manager.native_inner());
Ok(manager)
}
pub(crate) fn native_avatars(&self) -> Result<crate::AvatarManager, crate::Error> {
let mut cached = self
.runtime
.avatar_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Ok(crate::avatar_manager::AvatarManager::native_from_inner(
Arc::clone(inner),
));
}
let manager =
crate::avatar_manager::AvatarManager::native_new(Some(Arc::new(self.clone())))?;
*cached = Some(manager.native_inner());
Ok(manager)
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_avatars(&mut self, value: crate::AvatarManager) {
*self
.runtime
.avatar_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
pub(crate) fn native_animesh(&self) -> Result<crate::animesh::AnimeshManager, crate::Error> {
let mut cached = self
.runtime
.animesh_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Ok(crate::animesh_runtime::AnimeshManager::native_from_inner(
Arc::clone(inner),
));
}
let manager = crate::animesh_runtime::AnimeshManager::native_new(self.clone())?;
*cached = Some(manager.native_inner());
Ok(manager)
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_animesh(&mut self, value: crate::animesh::AnimeshManager) {
*self
.runtime
.animesh_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_assets(&mut self, value: crate::AssetManager) {
*self
.runtime
.asset_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
pub(crate) fn native_ais_client(&self) -> Result<crate::InventoryAISClient, crate::Error> {
let mut cached = self
.runtime
.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(client) = cached.as_ref() {
return Ok(client.clone());
}
let client =
crate::inventory_ais::InventoryAISClient::native_new(Some(Arc::new(self.clone())))?;
*cached = Some(client.clone());
Ok(client)
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_ais_client(&mut self, value: crate::InventoryAISClient) {
*self
.runtime
.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
#[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
pub(crate) fn native_set_inventory(&mut self, value: crate::InventoryManager) {
*self
.runtime
.inventory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
#[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
pub(crate) fn native_set_network(&mut self, value: crate::NetworkManager) {
*self
.runtime
.network_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value.native_inner_weak();
self.network_manager = Some(value);
}
pub(crate) fn native_self(&mut self) -> &mut crate::agent_manager::AgentManager {
if self.agent_manager.is_none() {
let client = Arc::new(self.clone());
let manager = crate::agent_manager::AgentManager::native_new(Some(client))
.unwrap_or_else(|_| panic!("failed to construct AgentManager"));
self.agent_manager = Some(Box::new(manager));
}
self.agent_manager.as_deref_mut().expect("agent manager")
}
pub(crate) fn native_agent_manager(&self) -> Result<crate::AgentManager, crate::Error> {
crate::agent_manager::AgentManager::native_new(Some(Arc::new(self.clone())))
}
pub(crate) fn cached_agent_manager_inner(
&self,
) -> Option<Arc<crate::agent_manager::AgentManagerInner>> {
self.runtime
.agent_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.upgrade()
}
pub(crate) fn cache_agent_manager_inner(
&self,
inner: &Arc<crate::agent_manager::AgentManagerInner>,
) {
*self
.runtime
.agent_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::downgrade(inner);
}
pub(crate) fn cached_object_manager_inner(
&self,
) -> Option<Arc<crate::object_manager::ObjectManagerInner>> {
self.runtime
.object_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn cache_object_manager_inner(
&self,
inner: &Arc<crate::object_manager::ObjectManagerInner>,
) {
*self
.runtime
.object_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::clone(inner));
}
pub(crate) fn install_object_manager_inner(
&self,
candidate: Arc<crate::object_manager::ObjectManagerInner>,
) -> Arc<crate::object_manager::ObjectManagerInner> {
let mut cached = self
.runtime
.object_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_objects(&self) -> Result<crate::ObjectManager, crate::Error> {
if let Some(inner) = self.cached_object_manager_inner() {
return Ok(crate::ObjectManager { inner });
}
crate::object_manager::ObjectManagerInner::new(Some(Arc::new(self.clone())))
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_objects(&self, value: crate::ObjectManager) {
self.cache_object_manager_inner(&value.inner);
}
pub(crate) fn install_environment_manager_inner(
&self,
candidate: Arc<crate::environment_manager::EnvironmentManagerInner>,
) -> Arc<crate::environment_manager::EnvironmentManagerInner> {
let mut cached = self
.runtime
.environment_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_environment(&self) -> Result<crate::EnvironmentManager, crate::Error> {
if let Some(inner) = self
.runtime
.environment_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
{
return Ok(crate::EnvironmentManager { inner });
}
crate::environment_manager::EnvironmentManager::native_new(Arc::new(self.clone()))
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_environment(&self, value: crate::EnvironmentManager) {
*self
.runtime
.environment_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn install_estate_tools_inner(
&self,
candidate: Arc<crate::estate_tools::EstateToolsInner>,
) -> Arc<crate::estate_tools::EstateToolsInner> {
let mut cached = self
.runtime
.estate_tools
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_estate(&self) -> Result<crate::EstateTools, crate::Error> {
if let Some(inner) = self
.runtime
.estate_tools
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
{
return Ok(crate::estate_tools::EstateTools::native_from_inner(inner));
}
crate::estate_tools::EstateTools::native_new(self.clone())
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_estate(&self, value: crate::EstateTools) {
*self
.runtime
.estate_tools
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
pub(crate) fn cached_terrain_manager_inner(
&self,
) -> Option<Arc<crate::terrain_manager::TerrainManagerInner>> {
self.runtime
.terrain_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_terrain_manager_inner(
&self,
candidate: Arc<crate::terrain_manager::TerrainManagerInner>,
) -> Arc<crate::terrain_manager::TerrainManagerInner> {
let mut cached = self
.runtime
.terrain_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_terrain(&self) -> Result<crate::TerrainManager, crate::Error> {
if let Some(inner) = self.cached_terrain_manager_inner() {
Ok(crate::TerrainManager { inner })
} else {
crate::terrain_manager::TerrainManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_terrain(&self, value: crate::TerrainManager) {
*self
.runtime
.terrain_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_sound_manager_inner(
&self,
) -> Option<Arc<crate::sound_manager::SoundManagerInner>> {
self.runtime
.sound_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_sound_manager_inner(
&self,
candidate: Arc<crate::sound_manager::SoundManagerInner>,
) -> Arc<crate::sound_manager::SoundManagerInner> {
let mut cached = self
.runtime
.sound_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_sound(&self) -> Result<crate::SoundManager, crate::Error> {
if let Some(inner) = self.cached_sound_manager_inner() {
Ok(crate::SoundManager { inner })
} else {
crate::sound_manager::SoundManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_sound(&self, value: crate::SoundManager) {
*self
.runtime
.sound_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_parcel_manager_inner(
&self,
) -> Option<Arc<crate::parcel_manager::ParcelManagerInner>> {
self.runtime
.parcel_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_parcel_manager_inner(
&self,
candidate: Arc<crate::parcel_manager::ParcelManagerInner>,
) -> Arc<crate::parcel_manager::ParcelManagerInner> {
let mut cached = self
.runtime
.parcel_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_parcels(&self) -> Result<crate::ParcelManager, crate::Error> {
if let Some(inner) = self.cached_parcel_manager_inner() {
Ok(crate::ParcelManager { inner })
} else {
crate::parcel_manager::ParcelManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_parcels(&self, value: crate::ParcelManager) {
*self
.runtime
.parcel_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_grid_manager_inner(
&self,
) -> Option<Arc<crate::grid_manager::GridManagerInner>> {
self.runtime
.grid_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_grid_manager_inner(
&self,
candidate: Arc<crate::grid_manager::GridManagerInner>,
) -> Arc<crate::grid_manager::GridManagerInner> {
let mut cached = self
.runtime
.grid_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_grid(&self) -> Result<crate::GridManager, crate::Error> {
if let Some(inner) = self.cached_grid_manager_inner() {
Ok(crate::GridManager { inner })
} else {
crate::grid_manager::GridManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_grid(&self, value: crate::GridManager) {
*self
.runtime
.grid_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_directory_manager_inner(
&self,
) -> Option<Arc<crate::directory_manager::DirectoryManagerInner>> {
self.runtime
.directory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_directory_manager_inner(
&self,
candidate: Arc<crate::directory_manager::DirectoryManagerInner>,
) -> Arc<crate::directory_manager::DirectoryManagerInner> {
let mut cached = self
.runtime
.directory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_directory(&self) -> Result<crate::DirectoryManager, crate::Error> {
if let Some(inner) = self.cached_directory_manager_inner() {
Ok(crate::DirectoryManager { inner })
} else {
crate::directory_manager::DirectoryManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_directory(&self, value: crate::DirectoryManager) {
*self
.runtime
.directory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_interest_list_manager_inner(
&self,
) -> Option<Arc<crate::interest_list::InterestListManagerInner>> {
self.runtime
.interest_list_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_interest_list_manager_inner(
&self,
candidate: Arc<crate::interest_list::InterestListManagerInner>,
) -> Arc<crate::interest_list::InterestListManagerInner> {
let mut cached = self
.runtime
.interest_list_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_interest_list(&self) -> Result<crate::InterestListManager, crate::Error> {
if let Some(inner) = self.cached_interest_list_manager_inner() {
Ok(crate::InterestListManager { inner })
} else {
crate::interest_list::InterestListManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_interest_list(&self, value: crate::InterestListManager) {
*self
.runtime
.interest_list_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_friends_manager_inner(
&self,
) -> Option<Arc<crate::friends_manager::FriendsManagerInner>> {
self.runtime
.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_friends_manager_inner(
&self,
candidate: Arc<crate::friends_manager::FriendsManagerInner>,
) -> Arc<crate::friends_manager::FriendsManagerInner> {
let mut cached = self
.runtime
.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_friends(&self) -> Result<crate::FriendsManager, crate::Error> {
if let Some(inner) = self.cached_friends_manager_inner() {
Ok(crate::FriendsManager { inner })
} else {
crate::friends_manager::FriendsManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_friends(&self, value: crate::FriendsManager) {
*self
.runtime
.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn cached_group_manager_inner(
&self,
) -> Option<Arc<crate::group_manager::GroupManagerInner>> {
self.runtime
.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn install_group_manager_inner(
&self,
candidate: Arc<crate::group_manager::GroupManagerInner>,
) -> Arc<crate::group_manager::GroupManagerInner> {
let mut cached = self
.runtime
.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return Arc::clone(inner);
}
*cached = Some(Arc::clone(&candidate));
candidate
}
pub(crate) fn native_groups(&self) -> crate::GroupManager {
if let Some(inner) = self.cached_group_manager_inner() {
crate::GroupManager { inner }
} else {
crate::group_manager::GroupManager::native_new(Arc::new(self.clone()))
}
}
pub(crate) fn native_set_groups(&self, value: crate::GroupManager) {
*self
.runtime
.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
}
pub(crate) fn native_marketplace(&self) -> crate::marketplace_runtime::MarketplaceManager {
let mut cached = self
.runtime
.marketplace_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(manager) = cached.as_ref() {
return manager.clone();
}
let manager = crate::marketplace_runtime::MarketplaceManager::native_new_client_owned(self);
*cached = Some(manager.clone());
manager
}
pub(crate) fn native_set_marketplace(
&self,
value: crate::marketplace_runtime::MarketplaceManager,
) {
value.native_bind_to_client(self);
*self
.runtime
.marketplace_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
pub(crate) fn native_set_caps_rate_limiter(
&mut self,
value: crate::caps_http::CapsRateLimiter,
) {
self.native_http_caps_client()
.set_rate_limiter(Some(value.clone()));
*self
.runtime
.caps_rate_limiter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
}
pub(crate) fn native_stats(&self) -> crate::stats::UtilizationStatistics {
self.runtime
.utilization_statistics
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn native_set_stats(&self, value: crate::stats::UtilizationStatistics) {
*self
.runtime
.utilization_statistics
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
}
pub(crate) fn native_throttle(&self) -> crate::AgentThrottle {
self.runtime
.throttle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn native_set_throttle(&self, value: crate::AgentThrottle) {
*self
.runtime
.throttle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
}
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
self.runtime.shutdown()
}
#[cfg(test)]
pub(crate) fn retention_probe(&self) -> ClientRetentionProbe {
ClientRetentionProbe(Arc::downgrade(&self.runtime))
}
}
impl fmt::Debug for GridClient {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let service_names: Vec<_> = self
.runtime
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.map(|entry| entry.service.name())
.collect();
formatter
.debug_struct("GridClient")
.field("settings", &self.settings)
.field("lifecycle_state", &self.lifecycle_state())
.field("services", &service_names)
.field("time_provider", &self.time_provider)
.finish_non_exhaustive()
}
}
impl Drop for GridClient {
fn drop(&mut self) {
if Arc::strong_count(&self.runtime) == 1 {
let _ = self.runtime.shutdown();
}
}
}
impl crate::IGridClient for GridClient {
fn ais_client(&self) -> crate::InventoryAISClient {
self.native_ais_client()
.unwrap_or_else(|_| panic!("failed to construct InventoryAISClient"))
}
fn appearance(&self) -> crate::AppearanceManager {
self.native_appearance()
.unwrap_or_else(|_| panic!("failed to construct AppearanceManager"))
}
fn assets(&self) -> crate::AssetManager {
self.native_assets()
.unwrap_or_else(|_| panic!("failed to construct AssetManager"))
}
fn avatars(&self) -> crate::AvatarManager {
self.native_avatars()
.unwrap_or_else(|_| panic!("failed to construct AvatarManager"))
}
fn directory(&self) -> crate::DirectoryManager {
self.native_directory()
.unwrap_or_else(|_| panic!("failed to construct DirectoryManager"))
}
fn environment(&self) -> crate::EnvironmentManager {
self.native_environment()
.unwrap_or_else(|_| panic!("failed to construct EnvironmentManager"))
}
fn estate(&self) -> crate::EstateTools {
self.native_estate()
.unwrap_or_else(|_| panic!("failed to construct EstateTools"))
}
fn friends(&self) -> crate::FriendsManager {
self.native_friends()
.unwrap_or_else(|_| panic!("failed to construct FriendsManager"))
}
fn grid(&self) -> crate::GridManager {
self.native_grid()
.unwrap_or_else(|_| panic!("failed to construct GridManager"))
}
fn groups(&self) -> crate::GroupManager {
self.native_groups()
}
fn http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
self.native_http_caps_client()
}
fn interest_list(&self) -> crate::InterestListManager {
self.native_interest_list()
.unwrap_or_else(|_| panic!("failed to construct InterestListManager"))
}
fn inventory(&self) -> crate::InventoryManager {
self.native_inventory()
.unwrap_or_else(|_| panic!("failed to construct InventoryManager"))
}
fn marketplace(&self) -> crate::marketplace::MarketplaceManager {
self.native_marketplace()
}
fn network(&self) -> crate::NetworkManager {
self.native_network()
.unwrap_or_else(|_| panic!("failed to construct NetworkManager"))
}
fn objects(&self) -> crate::ObjectManager {
self.native_objects()
.unwrap_or_else(|_| panic!("failed to construct ObjectManager"))
}
fn parcels(&self) -> crate::ParcelManager {
self.native_parcels()
.unwrap_or_else(|_| panic!("failed to construct ParcelManager"))
}
fn self_(&self) -> crate::AgentManager {
self.native_agent_manager()
.unwrap_or_else(|_| panic!("failed to construct AgentManager"))
}
fn settings(&self) -> Settings {
self.settings.clone()
}
fn sound(&self) -> crate::SoundManager {
self.native_sound()
.unwrap_or_else(|_| panic!("failed to construct SoundManager"))
}
fn stats(&self) -> crate::stats::UtilizationStatistics {
self.native_stats()
}
fn terrain(&self) -> crate::TerrainManager {
self.native_terrain()
.unwrap_or_else(|_| panic!("failed to construct TerrainManager"))
}
fn throttle(&self) -> crate::AgentThrottle {
self.native_throttle()
}
fn time_provider(&self) -> TimeProvider {
self.time_provider.clone()
}
fn set_time_provider(&mut self, value: TimeProvider) {
self.time_provider = value;
}
}
/// Explicit, runtime-neutral composition entry point for [`GridClient`].
pub struct GridClientBuilder {
settings: Settings,
time_provider: TimeProvider,
services: Vec<Arc<dyn ClientService>>,
}
impl Default for GridClientBuilder {
fn default() -> Self {
Self {
settings: Settings::defaults(),
time_provider: TimeProvider::system(),
services: Vec::new(),
}
}
}
impl GridClientBuilder {
#[must_use]
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = settings;
self
}
#[must_use]
pub fn with_time_provider(mut self, time_provider: TimeProvider) -> Self {
self.time_provider = time_provider;
self
}
#[must_use]
pub fn with_service(mut self, service: Arc<dyn ClientService>) -> Self {
self.services.push(service);
self
}
/// Validates configuration and creates a client without starting I/O.
///
/// # Errors
///
/// Returns the first invalid configuration field.
pub fn build(self) -> Result<GridClient, ClientCoreError> {
self.settings.validate()?;
let caps_rate_limiter = crate::caps_http::CapsRateLimiter::new_with_clock_and_overrides(
self.time_provider.clone(),
None,
)
.map_err(|_| ClientCoreError::InvalidConfiguration {
field: "CapsRateLimiter",
reason: "could not construct the configured token buckets",
})?;
let max_connections = usize::try_from(Settings::max_http_connections()).map_err(|_| {
ClientCoreError::InvalidConfiguration {
field: "Settings.MaxHttpConnections",
reason: "must be positive",
}
})?;
let caps_timeout = u64::try_from(self.settings.timing.caps_timeout).map_err(|_| {
ClientCoreError::InvalidConfiguration {
field: "Timing.CapsTimeout",
reason: "must be positive",
}
})?;
let http_caps_client = crate::caps_http::HttpCapsClient::production(
&Settings::user_agent(),
std::time::Duration::from_millis(caps_timeout),
max_connections,
caps_rate_limiter.clone(),
)
.map_err(|_| ClientCoreError::InvalidConfiguration {
field: "HttpCapsClient",
reason: "could not construct the HTTP transport",
})?;
Ok(GridClient {
settings: self.settings,
time_provider: self.time_provider,
runtime: Arc::new(ClientRuntime::new(
self.services,
http_caps_client,
caps_rate_limiter,
)),
network_manager: None,
agent_manager: None,
})
}
}
/// Native storage for the mapped grouped settings object.
#[derive(Clone, PartialEq)]
pub struct Settings {
pub default_effect_color: libremetaverse_types::Color4,
pub(crate) connection: ConnectionSettings,
pub(crate) timing: TimingSettings,
pub(crate) packets: PacketSettings,
pub(crate) agent: AgentSettings,
pub(crate) world: WorldSettings,
pub(crate) parcel: ParcelSettings,
pub(crate) asset_cache: AssetCacheSettings,
pub(crate) texture_pipeline: TexturePipelineSettings,
pub(crate) logging: LoggingSettings,
pub(crate) upload_cost: i32,
}
impl Settings {
pub(crate) fn defaults() -> Self {
Self {
default_effect_color: libremetaverse_types::Color4 {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
connection: connection_settings_defaults(),
timing: timing_settings_defaults(),
packets: packet_settings_defaults(),
agent: agent_settings_defaults(),
world: world_settings_defaults(),
parcel: parcel_settings_defaults(),
asset_cache: asset_cache_settings_defaults(),
texture_pipeline: texture_pipeline_settings_defaults(),
logging: logging_settings_defaults(),
upload_cost: 0,
}
}
/// Mutable access to the mapped agent settings for native composition.
pub fn agent_settings_mut(&mut self) -> &mut AgentSettings {
&mut self.agent
}
#[must_use]
pub fn connection_mut(&mut self) -> &mut ConnectionSettings {
&mut self.connection
}
#[must_use]
pub fn packets_mut(&mut self) -> &mut PacketSettings {
&mut self.packets
}
#[must_use]
pub fn agent_mut(&mut self) -> &mut AgentSettings {
&mut self.agent
}
#[must_use]
pub fn world_mut(&mut self) -> &mut WorldSettings {
&mut self.world
}
#[must_use]
pub fn parcel_mut(&mut self) -> &mut ParcelSettings {
&mut self.parcel
}
#[must_use]
pub fn asset_cache_mut(&mut self) -> &mut AssetCacheSettings {
&mut self.asset_cache
}
#[must_use]
pub fn texture_pipeline_mut(&mut self) -> &mut TexturePipelineSettings {
&mut self.texture_pipeline
}
#[must_use]
pub fn logging_mut(&mut self) -> &mut LoggingSettings {
&mut self.logging
}
/// Validates all durations, limits, endpoints, cache, and feature policies.
///
/// # Errors
///
/// Returns the first field that cannot be used safely by the native client.
pub fn validate(&self) -> Result<(), ClientCoreError> {
validate_endpoint("Connection.LoginServer", &self.connection.login_server)?;
for (field, value) in [
("Timing.TransferTimeout", self.timing.transfer_timeout),
("Timing.TeleportTimeout", self.timing.teleport_timeout),
("Timing.LogoutTimeout", self.timing.logout_timeout),
("Timing.CapsTimeout", self.timing.caps_timeout),
("Timing.LoginTimeout", self.timing.login_timeout),
("Timing.ResendTimeout", self.timing.resend_timeout),
("Timing.SimulatorTimeout", self.timing.simulator_timeout),
("Timing.MapRequestTimeout", self.timing.map_request_timeout),
(
"Timing.AgentUpdateInterval",
self.timing.agent_update_interval,
),
(
"Timing.InterpolationInterval",
self.timing.interpolation_interval,
),
] {
validate_positive(field, value)?;
}
validate_positive("Packets.MaxPendingAcks", self.packets.max_pending_acks)?;
validate_positive("Packets.StatsQueueSize", self.packets.stats_queue_size)?;
if self.packets.max_resend_count < 0 {
return invalid("Packets.MaxResendCount", "must be zero or greater");
}
if self.agent.send_updates_regularly && !self.agent.send_updates {
return invalid("Agent.SendUpdatesRegularly", "requires Agent.SendUpdates");
}
if (self.parcel.always_request_acl || self.parcel.always_request_dwell)
&& !self.parcel.track_parcels
{
return invalid(
"Parcel.TrackParcels",
"must be enabled for automatic parcel requests",
);
}
validate_positive(
"TexturePipeline.MaxConcurrentDownloads",
self.texture_pipeline.max_concurrent_downloads,
)?;
validate_positive(
"TexturePipeline.RequestTimeout",
self.texture_pipeline.request_timeout,
)?;
if self.asset_cache.enabled {
if self.asset_cache.dir.trim().is_empty() {
return invalid(
"AssetCache.Dir",
"must not be empty when caching is enabled",
);
}
if self.asset_cache.max_size <= 0 {
return invalid(
"AssetCache.MaxSize",
"must be positive when caching is enabled",
);
}
}
Ok(())
}
}
impl Default for Settings {
fn default() -> Self {
Self::defaults()
}
}
impl fmt::Debug for Settings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Settings")
.field("default_effect_color", &self.default_effect_color)
.field("connection", &self.connection)
.field("timing", &self.timing)
.field("packets", &self.packets)
.field("agent", &self.agent)
.field("world", &self.world)
.field("parcel", &self.parcel)
.field("asset_cache", &self.asset_cache)
.field("texture_pipeline", &self.texture_pipeline)
.field("logging", &self.logging)
.field("upload_cost", &self.upload_cost)
.finish()
}
}
impl fmt::Debug for ConnectionSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ConnectionSettings")
.field("login_server", &"<redacted endpoint>")
.field("mfa_enabled", &self.mfa_enabled)
.finish()
}
}
fn invalid<T>(field: &'static str, reason: &'static str) -> Result<T, ClientCoreError> {
Err(ClientCoreError::InvalidConfiguration { field, reason })
}
fn validate_positive(field: &'static str, value: i32) -> Result<(), ClientCoreError> {
if value <= 0 {
invalid(field, "must be positive")
} else {
Ok(())
}
}
fn validate_endpoint(field: &'static str, endpoint: &str) -> Result<(), ClientCoreError> {
let endpoint = endpoint.trim();
let remainder = endpoint
.strip_prefix("https://")
.or_else(|| endpoint.strip_prefix("http://"))
.ok_or(ClientCoreError::InvalidConfiguration {
field,
reason: "must use an HTTP or HTTPS URL",
})?;
let authority = remainder.split(['/', '?', '#']).next().unwrap_or_default();
if authority.is_empty() || authority.contains('@') || endpoint.chars().any(char::is_whitespace)
{
return invalid(
field,
"must contain a host and must not embed credentials or whitespace",
);
}
Ok(())
}
pub(crate) fn agent_settings_defaults() -> AgentSettings {
AgentSettings {
disable_update_duplicate_check: true,
multiple_sims: false,
send_appearance: true,
send_throttle: true,
send_updates: true,
send_updates_regularly: true,
}
}
pub(crate) fn asset_cache_settings_defaults() -> AssetCacheSettings {
AssetCacheSettings {
dir: std::path::Path::new("linden")
.join("cache")
.to_string_lossy()
.into_owned(),
enabled: true,
max_size: 1_024 * 1_024 * 1_024,
}
}
pub(crate) fn connection_settings_defaults() -> ConnectionSettings {
ConnectionSettings {
login_server: Settings::AGNI_LOGIN_SERVER.to_owned(),
mfa_enabled: false,
}
}
pub(crate) fn logging_settings_defaults() -> LoggingSettings {
LoggingSettings {
log_disk_cache: true,
log_names: true,
log_resends: true,
}
}
pub(crate) fn packet_settings_defaults() -> PacketSettings {
PacketSettings {
enable_sim_stats: true,
max_pending_acks: 10,
max_resend_count: 3,
send_pings: true,
stats_queue_size: 5,
throttle_outgoing: true,
track_utilization: false,
}
}
pub(crate) fn parcel_settings_defaults() -> ParcelSettings {
ParcelSettings {
always_request_acl: true,
always_request_dwell: true,
pool_parcel_data: false,
track_parcels: true,
}
}
pub(crate) fn texture_pipeline_settings_defaults() -> TexturePipelineSettings {
TexturePipelineSettings {
enabled: true,
max_concurrent_downloads: 4,
request_timeout: 45_000,
use_http_textures: true,
}
}
pub(crate) fn timing_settings_defaults() -> TimingSettings {
TimingSettings {
agent_update_interval: 500,
caps_timeout: 60_000,
interpolation_interval: 250,
login_timeout: 60_000,
logout_timeout: 5_000,
map_request_timeout: 5_000,
resend_timeout: 4_000,
simulator_timeout: 30_000,
teleport_timeout: 40_000,
transfer_timeout: 90_000,
}
}
pub(crate) fn world_settings_defaults() -> WorldSettings {
WorldSettings {
always_decode_objects: true,
always_request_objects: true,
cache_primitives: false,
store_land_patches: false,
track_avatars: true,
track_objects: true,
use_interpolation_timer: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RecordingService {
name: &'static str,
phase: ShutdownPhase,
order: Arc<Mutex<Vec<&'static str>>>,
calls: AtomicUsize,
}
impl ClientService for RecordingService {
fn name(&self) -> &'static str {
self.name
}
fn shutdown_phase(&self) -> ShutdownPhase {
self.phase
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
self.calls.fetch_add(1, Ordering::Relaxed);
self.order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(self.name);
Ok(())
}
}
struct ReentrantService {
client: Mutex<Option<GridClient>>,
calls: AtomicUsize,
}
impl ClientService for ReentrantService {
fn name(&self) -> &'static str {
"reentrant"
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
self.calls.fetch_add(1, Ordering::AcqRel);
let result = self
.client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
.expect("client installed")
.register_service(Arc::new(RecordingService {
name: "late",
phase: ShutdownPhase::Manager,
order: Arc::new(Mutex::new(Vec::new())),
calls: AtomicUsize::new(0),
}));
assert!(matches!(
result,
Err(ClientCoreError::InvalidLifecycle {
state: ClientLifecycleState::ShuttingDown,
..
})
));
Ok(())
}
}
#[test]
#[allow(clippy::float_cmp)] // Exact representable constants are the compatibility contract.
fn defaults_match_reference_and_validate() {
let settings = Settings::defaults();
assert_eq!(
settings.connection.login_server,
Settings::AGNI_LOGIN_SERVER
);
assert_eq!(settings.timing.transfer_timeout, 90_000);
assert_eq!(settings.timing.teleport_timeout, 40_000);
assert_eq!(settings.timing.logout_timeout, 5_000);
assert_eq!(settings.timing.caps_timeout, 60_000);
assert_eq!(settings.timing.login_timeout, 60_000);
assert_eq!(settings.timing.resend_timeout, 4_000);
assert_eq!(settings.timing.simulator_timeout, 30_000);
assert_eq!(settings.timing.map_request_timeout, 5_000);
assert_eq!(settings.timing.agent_update_interval, 500);
assert_eq!(settings.timing.interpolation_interval, 250);
assert_eq!(settings.packets.max_pending_acks, 10);
assert_eq!(settings.packets.stats_queue_size, 5);
assert_eq!(settings.packets.max_resend_count, 3);
assert!(settings.packets.throttle_outgoing);
assert!(settings.packets.enable_sim_stats);
assert!(settings.packets.send_pings);
assert!(!settings.packets.track_utilization);
assert!(settings.agent.send_updates);
assert!(settings.agent.send_updates_regularly);
assert!(settings.agent.send_appearance);
assert!(settings.agent.send_throttle);
assert!(settings.agent.disable_update_duplicate_check);
assert!(!settings.agent.multiple_sims);
assert!(settings.world.always_decode_objects);
assert!(settings.world.always_request_objects);
assert!(settings.world.track_objects);
assert!(settings.world.track_avatars);
assert!(!settings.world.cache_primitives);
assert!(settings.world.use_interpolation_timer);
assert!(!settings.world.store_land_patches);
assert!(settings.parcel.track_parcels);
assert!(settings.parcel.always_request_acl);
assert!(settings.parcel.always_request_dwell);
assert!(!settings.parcel.pool_parcel_data);
assert!(settings.asset_cache.enabled);
assert!(settings.asset_cache.dir.ends_with("cache"));
assert_eq!(settings.texture_pipeline.max_concurrent_downloads, 4);
assert!(settings.texture_pipeline.enabled);
assert!(settings.texture_pipeline.use_http_textures);
assert_eq!(settings.texture_pipeline.request_timeout, 45_000);
assert!(settings.logging.log_names);
assert!(settings.logging.log_resends);
assert!(settings.logging.log_disk_cache);
assert_eq!(settings.asset_cache.max_size, 1_073_741_824);
assert_eq!(settings.default_effect_color.r, 1.0);
assert_eq!(settings.upload_cost, 0);
assert_eq!(settings.validate(), Ok(()));
assert_eq!(Settings::PING_INTERVAL, 2_200);
assert_eq!(Settings::NETWORK_TICK_INTERVAL, 500);
assert_eq!(Settings::MAX_PACKET_SIZE, 1_200);
assert_eq!(Settings::MAX_SEQUENCE, 0xFF_FFFF);
assert_eq!(Settings::user_agent(), "LibreMetaverse");
assert_eq!(Settings::resource_dir(), "linden");
assert_eq!(Settings::max_http_connections(), 32);
assert_eq!(Settings::packet_archive_size(), 1_000);
assert_eq!(Settings::udp_receive_queue_capacity(), 512);
assert_eq!(Settings::texture_pipeline_refresh_interval(), 500.0);
assert_eq!(Settings::simulator_pool_timeout(), 120_000);
assert!(Settings::bind_address().is_unspecified());
}
#[test]
fn validation_reports_the_exact_invalid_field() {
let mut settings = Settings::defaults();
settings.timing.login_timeout = 0;
assert_eq!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Timing.LoginTimeout",
reason: "must be positive",
})
);
let mut settings = Settings::defaults();
settings.connection.login_server = "https://user:secret@example.test/login".to_owned();
assert!(matches!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Connection.LoginServer",
..
})
));
assert!(!format!("{settings:?}").contains("secret"));
let mut settings = Settings::defaults();
settings.agent.send_updates = false;
assert_eq!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Agent.SendUpdatesRegularly",
reason: "requires Agent.SendUpdates",
})
);
}
#[test]
fn construction_is_inert_and_shutdown_is_ordered_and_idempotent() {
let order = Arc::new(Mutex::new(Vec::new()));
let manager = Arc::new(RecordingService {
name: "manager",
phase: ShutdownPhase::Manager,
order: Arc::clone(&order),
calls: AtomicUsize::new(0),
});
let network = Arc::new(RecordingService {
name: "network",
phase: ShutdownPhase::Network,
order: Arc::clone(&order),
calls: AtomicUsize::new(0),
});
let client = GridClient::builder()
.with_service(manager.clone())
.with_service(network.clone())
.build()
.unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Active);
assert!(!client.cancellation_token().is_cancellation_requested());
client.shutdown().unwrap();
client.shutdown().unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
assert!(client.cancellation_token().is_cancellation_requested());
assert_eq!(
*order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec!["network", "manager"]
);
assert_eq!(network.calls.load(Ordering::Relaxed), 1);
assert_eq!(manager.calls.load(Ordering::Relaxed), 1);
}
#[test]
fn shutdown_does_not_hold_service_registry_lock_across_callbacks() {
let service = Arc::new(ReentrantService {
client: Mutex::new(None),
calls: AtomicUsize::new(0),
});
let client = GridClient::builder()
.with_service(service.clone())
.build()
.unwrap();
*service
.client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());
client.shutdown().unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
assert_eq!(service.calls.load(Ordering::Acquire), 1);
service
.client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
}