1709 lines
58 KiB
Rust
1709 lines
58 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)]
|
|
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>,
|
|
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>>>,
|
|
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>>>,
|
|
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<()>,
|
|
}
|
|
|
|
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),
|
|
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),
|
|
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),
|
|
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();
|
|
let mut services = 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.iter() {
|
|
if let Err(error) = entry.service.shutdown()
|
|
&& first_error.is_none()
|
|
{
|
|
first_error = Some(error);
|
|
}
|
|
}
|
|
services.clear();
|
|
self.agent_throttle_sender
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.inventory_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.inventory_ais_client
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.object_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.grid_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.directory_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.interest_list_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.friends_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
self.group_manager
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.take();
|
|
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 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 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_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 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 http_caps_client(&self) -> crate::caps_http::HttpCapsClient {
|
|
self.native_http_caps_client()
|
|
}
|
|
|
|
fn settings(&self) -> Settings {
|
|
self.settings.clone()
|
|
}
|
|
|
|
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(())
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|