Implement native client core lifecycle (#51)
This commit is contained in:
@@ -12,9 +12,10 @@ use std::hash::Hash;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, Weak,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
pub trait Collection<T> {}
|
||||
|
||||
@@ -289,13 +290,179 @@ pub struct XmlTextReader(pub String);
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct XmlTextWriter(pub String);
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CancellationToken(Arc<AtomicBool>);
|
||||
struct CancellationState {
|
||||
cancelled: AtomicBool,
|
||||
next_id: AtomicU64,
|
||||
waiters: Mutex<BTreeMap<u64, Waker>>,
|
||||
callbacks: Mutex<BTreeMap<u64, Arc<dyn Fn() + Send + Sync>>>,
|
||||
}
|
||||
|
||||
impl Default for CancellationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cancelled: AtomicBool::new(false),
|
||||
next_id: AtomicU64::new(1),
|
||||
waiters: Mutex::new(BTreeMap::new()),
|
||||
callbacks: Mutex::new(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cheap, cloneable cancellation signal.
|
||||
///
|
||||
/// Waiting is executor-neutral: no thread or async runtime is created by this
|
||||
/// type. Cancelling wakes every registered future and invokes linked-token
|
||||
/// callbacks after releasing internal locks.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CancellationToken(Arc<CancellationState>);
|
||||
|
||||
impl fmt::Debug for CancellationToken {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("CancellationToken")
|
||||
.field(
|
||||
"is_cancellation_requested",
|
||||
&self.is_cancellation_requested(),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl CancellationToken {
|
||||
#[must_use]
|
||||
pub fn is_cancellation_requested(&self) -> bool {
|
||||
self.0.load(Ordering::Acquire)
|
||||
self.0.cancelled.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Returns a future that completes when cancellation is requested.
|
||||
#[must_use]
|
||||
pub fn cancelled(&self) -> CancellationFuture {
|
||||
CancellationFuture {
|
||||
state: Arc::clone(&self.0),
|
||||
waiter_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an observed cancellation request to the shared typed error.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::Cancelled`] after the token has been cancelled.
|
||||
pub fn throw_if_cancellation_requested(&self) -> Result<(), crate::Error> {
|
||||
if self.is_cancellation_requested() {
|
||||
Err(crate::Error::Cancelled)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn register(&self, callback: Arc<dyn Fn() + Send + Sync>) -> CancellationRegistration {
|
||||
if self.is_cancellation_requested() {
|
||||
callback();
|
||||
return CancellationRegistration::completed();
|
||||
}
|
||||
|
||||
let id = self.0.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.0
|
||||
.callbacks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(id, callback);
|
||||
if self.is_cancellation_requested() {
|
||||
let callback = self
|
||||
.0
|
||||
.callbacks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&id);
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
}
|
||||
CancellationRegistration::completed()
|
||||
} else {
|
||||
CancellationRegistration {
|
||||
state: Arc::downgrade(&self.0),
|
||||
id: Some(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Future returned by [`CancellationToken::cancelled`].
|
||||
pub struct CancellationFuture {
|
||||
state: Arc<CancellationState>,
|
||||
waiter_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Future for CancellationFuture {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.state.cancelled.load(Ordering::Acquire) {
|
||||
return Poll::Ready(());
|
||||
}
|
||||
|
||||
let id = if let Some(id) = self.waiter_id {
|
||||
id
|
||||
} else {
|
||||
let id = self.state.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.waiter_id = Some(id);
|
||||
id
|
||||
};
|
||||
self.state
|
||||
.waiters
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(id, context.waker().clone());
|
||||
if self.state.cancelled.load(Ordering::Acquire) {
|
||||
self.state
|
||||
.waiters
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&id);
|
||||
self.waiter_id = None;
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CancellationFuture {
|
||||
fn drop(&mut self) {
|
||||
if let Some(id) = self.waiter_id {
|
||||
self.state
|
||||
.waiters
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CancellationRegistration {
|
||||
state: Weak<CancellationState>,
|
||||
id: Option<u64>,
|
||||
}
|
||||
|
||||
impl CancellationRegistration {
|
||||
fn completed() -> Self {
|
||||
Self {
|
||||
state: Weak::new(),
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CancellationRegistration {
|
||||
fn drop(&mut self) {
|
||||
if let (Some(state), Some(id)) = (self.state.upgrade(), self.id) {
|
||||
state
|
||||
.callbacks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,8 +477,35 @@ pub struct DictionaryEntry(pub Object, pub Object);
|
||||
|
||||
pub struct RateLimitLease;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CancellationTokenSource(CancellationToken);
|
||||
struct CancellationSourceState {
|
||||
token: CancellationToken,
|
||||
linked_registrations: Mutex<Vec<CancellationRegistration>>,
|
||||
}
|
||||
|
||||
/// Owns and triggers a [`CancellationToken`].
|
||||
#[derive(Clone)]
|
||||
pub struct CancellationTokenSource(Arc<CancellationSourceState>);
|
||||
|
||||
impl Default for CancellationTokenSource {
|
||||
fn default() -> Self {
|
||||
Self(Arc::new(CancellationSourceState {
|
||||
token: CancellationToken::default(),
|
||||
linked_registrations: Mutex::new(Vec::new()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CancellationTokenSource {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("CancellationTokenSource")
|
||||
.field(
|
||||
"is_cancellation_requested",
|
||||
&self.0.token.is_cancellation_requested(),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl CancellationTokenSource {
|
||||
#[must_use]
|
||||
@@ -319,13 +513,62 @@ impl CancellationTokenSource {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Creates a source cancelled when any parent token is cancelled.
|
||||
#[must_use]
|
||||
pub fn new_linked(tokens: &[CancellationToken]) -> Self {
|
||||
let source = Self::new();
|
||||
let mut registrations = Vec::with_capacity(tokens.len());
|
||||
for token in tokens {
|
||||
let linked = Arc::downgrade(&source.0);
|
||||
registrations.push(token.register(Arc::new(move || {
|
||||
if let Some(linked) = linked.upgrade() {
|
||||
Self::cancel_state(&linked);
|
||||
}
|
||||
})));
|
||||
}
|
||||
*source
|
||||
.0
|
||||
.linked_registrations
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = registrations;
|
||||
source
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn token(&self) -> CancellationToken {
|
||||
self.0.clone()
|
||||
self.0.token.clone()
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.0.0.store(true, Ordering::Release);
|
||||
Self::cancel_state(&self.0);
|
||||
}
|
||||
|
||||
fn cancel_state(state: &CancellationSourceState) {
|
||||
if state.token.0.cancelled.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
let waiters = std::mem::take(
|
||||
&mut *state
|
||||
.token
|
||||
.0
|
||||
.waiters
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
);
|
||||
let callbacks = std::mem::take(
|
||||
&mut *state
|
||||
.token
|
||||
.0
|
||||
.callbacks
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner),
|
||||
);
|
||||
for (_, waker) in waiters {
|
||||
waker.wake();
|
||||
}
|
||||
for (_, callback) in callbacks {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +590,43 @@ pub struct Task<T>(pub PhantomData<fn() -> T>);
|
||||
|
||||
pub struct TaskCompletionSource<T>(pub PhantomData<fn(T)>);
|
||||
|
||||
pub struct TimeProvider;
|
||||
/// Cloneable clock boundary used by runtime-neutral client code.
|
||||
#[derive(Clone)]
|
||||
pub struct TimeProvider(Arc<dyn Fn() -> std::time::SystemTime + Send + Sync>);
|
||||
|
||||
impl TimeProvider {
|
||||
/// Returns the process system clock.
|
||||
#[must_use]
|
||||
pub fn system() -> Self {
|
||||
Self(Arc::new(std::time::SystemTime::now))
|
||||
}
|
||||
|
||||
/// Creates an injected clock for deterministic tests and embedders.
|
||||
#[must_use]
|
||||
pub fn from_fn<F>(clock: F) -> Self
|
||||
where
|
||||
F: Fn() -> std::time::SystemTime + Send + Sync + 'static,
|
||||
{
|
||||
Self(Arc::new(clock))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_utc_now(&self) -> std::time::SystemTime {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimeProvider {
|
||||
fn default() -> Self {
|
||||
Self::system()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TimeProvider {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("TimeProvider(<injected clock>)")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MediaTypeHeaderValue(pub String);
|
||||
|
||||
@@ -459,11 +738,44 @@ pub struct SocketException;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CancellationToken, HttpMessageHandler, HttpRequest, HttpResponse, Object, Uri};
|
||||
use super::{
|
||||
CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse,
|
||||
Object, TimeProvider, Uri,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
#[test]
|
||||
fn cancellation_wakes_waiters_and_propagates_from_linked_tokens() {
|
||||
let parent = CancellationTokenSource::new();
|
||||
let linked = CancellationTokenSource::new_linked(&[parent.token()]);
|
||||
let mut future = Box::pin(linked.token().cancelled());
|
||||
let mut context = Context::from_waker(Waker::noop());
|
||||
assert!(matches!(future.as_mut().poll(&mut context), Poll::Pending));
|
||||
|
||||
parent.cancel();
|
||||
assert!(linked.token().is_cancellation_requested());
|
||||
assert!(matches!(
|
||||
future.as_mut().poll(&mut context),
|
||||
Poll::Ready(())
|
||||
));
|
||||
assert_eq!(
|
||||
linked.token().throw_if_cancellation_requested(),
|
||||
Err(crate::Error::Cancelled)
|
||||
);
|
||||
parent.cancel();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn injected_time_provider_is_deterministic_and_redacted() {
|
||||
let instant = SystemTime::UNIX_EPOCH + Duration::from_secs(42);
|
||||
let provider = TimeProvider::from_fn(move || instant);
|
||||
assert_eq!(provider.get_utc_now(), instant);
|
||||
assert!(!format!("{provider:?}").contains("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_handler_preserves_request_and_response_data() {
|
||||
|
||||
833
crates/libremetaverse/src/client_core.rs
Normal file
833
crates/libremetaverse/src/client_core.rs
Normal file
@@ -0,0 +1,833 @@
|
||||
//! 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>>,
|
||||
shutdown_complete: Condvar,
|
||||
shutdown_wait: Mutex<()>,
|
||||
}
|
||||
|
||||
impl ClientRuntime {
|
||||
fn new(services: Vec<Arc<dyn ClientService>>) -> 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(),
|
||||
),
|
||||
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();
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
|
||||
self.runtime.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GridClient {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.runtime.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::IGridClient for GridClient {
|
||||
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()?;
|
||||
Ok(GridClient {
|
||||
settings: self.settings,
|
||||
time_provider: self.time_provider,
|
||||
runtime: Arc::new(ClientRuntime::new(self.services)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3296,6 +3296,7 @@ impl AgentPreferencesEventArgs {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.AgentSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AgentSettings {
|
||||
/// C# member: `F:LibreMetaverse.AgentSettings.DisableUpdateDuplicateCheck`.
|
||||
pub disable_update_duplicate_check: bool,
|
||||
@@ -3313,7 +3314,8 @@ pub struct AgentSettings {
|
||||
impl AgentSettings {
|
||||
/// C# member: `M:LibreMetaverse.AgentSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.AgentSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::agent_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4976,6 +4978,7 @@ impl AssetCacheComputeAssetCacheFilenameDelegate {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.AssetCacheSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AssetCacheSettings {
|
||||
/// C# member: `F:LibreMetaverse.AssetCacheSettings.Dir`.
|
||||
pub dir: String,
|
||||
@@ -4987,7 +4990,8 @@ pub struct AssetCacheSettings {
|
||||
impl AssetCacheSettings {
|
||||
/// C# member: `M:LibreMetaverse.AssetCacheSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.AssetCacheSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::asset_cache_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7493,6 +7497,7 @@ impl CompressedFlags {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.ConnectionSettings`.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ConnectionSettings {
|
||||
/// C# member: `F:LibreMetaverse.ConnectionSettings.LoginServer`.
|
||||
pub login_server: String,
|
||||
@@ -7502,7 +7507,8 @@ pub struct ConnectionSettings {
|
||||
impl ConnectionSettings {
|
||||
/// C# member: `M:LibreMetaverse.ConnectionSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.ConnectionSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::connection_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10958,23 +10964,29 @@ pub use crate::foliage_catalog::GrassDefinition;
|
||||
pub use crate::foliage_catalog::GrassDefinitions;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.GridClient`.
|
||||
pub struct GridClient;
|
||||
pub use crate::client_core::GridClient;
|
||||
impl GridClient {
|
||||
/// C# member: `M:LibreMetaverse.GridClient.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.GridClient.#ctor")
|
||||
/* native client-core implementation */
|
||||
crate::client_core::GridClientBuilder::default()
|
||||
.build()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.GridClient.Dispose`.
|
||||
pub fn dispose_with_method(&self) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.GridClient.Dispose")
|
||||
/* native client-core implementation */
|
||||
self.shutdown().map_err(Into::into)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.GridClient.DisposeAsync`.
|
||||
pub async fn dispose_with_method_87ebceff(&self) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.GridClient.DisposeAsync")
|
||||
/* native client-core implementation */
|
||||
self.shutdown().map_err(Into::into)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.GridClient.ToString`.
|
||||
pub fn to_string(&self) -> String {
|
||||
libremetaverse_types::unimplemented_api!("M:LibreMetaverse.GridClient.ToString")
|
||||
/* native client-core implementation */
|
||||
String::new()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.GridClient.AisClient`.
|
||||
pub fn ais_client(&self) -> libremetaverse::InventoryAISClient {
|
||||
@@ -11134,7 +11146,8 @@ impl GridClient {
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.GridClient.Settings`.
|
||||
pub fn settings(&mut self) -> &mut libremetaverse::Settings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.Settings")
|
||||
/* native client-core implementation */
|
||||
&mut self.settings
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.GridClient.Sound`.
|
||||
pub fn sound(&self) -> libremetaverse::SoundManager {
|
||||
@@ -11170,14 +11183,15 @@ impl GridClient {
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.GridClient.TimeProvider`.
|
||||
pub fn time_provider(&self) -> libremetaverse_types::compat::TimeProvider {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.TimeProvider")
|
||||
/* native client-core implementation */
|
||||
self.time_provider.clone()
|
||||
}
|
||||
/// Setter for C# member: `P:LibreMetaverse.GridClient.TimeProvider`.
|
||||
pub fn set_time_provider(&mut self, value: libremetaverse_types::compat::TimeProvider) {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.TimeProvider")
|
||||
/* native client-core implementation */
|
||||
self.time_provider = value
|
||||
}
|
||||
}
|
||||
impl libremetaverse::IGridClient for GridClient {}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.GridClientBakingTextureProvider`.
|
||||
pub struct GridClientBakingTextureProvider;
|
||||
@@ -17618,6 +17632,7 @@ impl LoggerLogCallback {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.LoggingSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct LoggingSettings {
|
||||
/// C# member: `F:LibreMetaverse.LoggingSettings.LogDiskCache`.
|
||||
pub log_disk_cache: bool,
|
||||
@@ -17629,7 +17644,8 @@ pub struct LoggingSettings {
|
||||
impl LoggingSettings {
|
||||
/// C# member: `M:LibreMetaverse.LoggingSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.LoggingSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::logging_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21519,6 +21535,7 @@ impl PacketSentEventArgs {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.PacketSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PacketSettings {
|
||||
/// C# member: `F:LibreMetaverse.PacketSettings.EnableSimStats`.
|
||||
pub enable_sim_stats: bool,
|
||||
@@ -21538,7 +21555,8 @@ pub struct PacketSettings {
|
||||
impl PacketSettings {
|
||||
/// C# member: `M:LibreMetaverse.PacketSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.PacketSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::packet_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22666,6 +22684,7 @@ pub enum ParcelResult {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.ParcelSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ParcelSettings {
|
||||
/// C# member: `F:LibreMetaverse.ParcelSettings.AlwaysRequestAcl`.
|
||||
pub always_request_acl: bool,
|
||||
@@ -22679,7 +22698,8 @@ pub struct ParcelSettings {
|
||||
impl ParcelSettings {
|
||||
/// C# member: `M:LibreMetaverse.ParcelSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.ParcelSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::parcel_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25426,10 +25446,8 @@ impl SetDisplayNameReplyEventArgs {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Settings`.
|
||||
pub struct Settings {
|
||||
/// C# member: `F:LibreMetaverse.Settings.DefaultEffectColor`.
|
||||
pub default_effect_color: libremetaverse_types::Color4,
|
||||
}
|
||||
pub use crate::client_core::Settings;
|
||||
/// C# member: `F:LibreMetaverse.Settings.DefaultEffectColor`.
|
||||
impl Settings {
|
||||
/// C# member: `F:LibreMetaverse.Settings.AditiLoginServer`.
|
||||
pub const ADITI_LOGIN_SERVER: &'static str =
|
||||
@@ -25439,7 +25457,8 @@ impl Settings {
|
||||
"https://login.agni.lindenlab.com/cgi-bin/login.cgi";
|
||||
/// C# member: `F:LibreMetaverse.Settings.BindAddress`.
|
||||
pub fn bind_address() -> std::net::IpAddr {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.BindAddress")
|
||||
/* native client-core implementation */
|
||||
std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED)
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.EnableInventoryStore`.
|
||||
pub const ENABLE_INVENTORY_STORE: bool = true;
|
||||
@@ -25447,11 +25466,13 @@ impl Settings {
|
||||
pub const ENABLE_LIBRARY_STORE: bool = true;
|
||||
/// C# member: `F:LibreMetaverse.Settings.LogLevel`.
|
||||
pub fn log_level() -> libremetaverse::logging::LogLevel {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.LogLevel")
|
||||
/* native client-core implementation */
|
||||
crate::logging::LogLevel(1)
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.MaxHttpConnections`.
|
||||
pub fn max_http_connections() -> i32 {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.MaxHttpConnections")
|
||||
/* native client-core implementation */
|
||||
32
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.MaxPacketSize`.
|
||||
pub const MAX_PACKET_SIZE: i32 = 1200;
|
||||
@@ -25461,83 +25482,95 @@ impl Settings {
|
||||
pub const NETWORK_TICK_INTERVAL: i32 = 500;
|
||||
/// C# member: `F:LibreMetaverse.Settings.PacketArchiveSize`.
|
||||
pub fn packet_archive_size() -> i32 {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.PacketArchiveSize")
|
||||
/* native client-core implementation */
|
||||
1000
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.PingInterval`.
|
||||
pub const PING_INTERVAL: i32 = 2200;
|
||||
/// C# member: `F:LibreMetaverse.Settings.ResourceDir`.
|
||||
pub fn resource_dir() -> String {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.ResourceDir")
|
||||
/* native client-core implementation */
|
||||
"linden".to_owned()
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.SimulatorPoolTimeout`.
|
||||
pub fn simulator_pool_timeout() -> i32 {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.SimulatorPoolTimeout")
|
||||
/* native client-core implementation */
|
||||
120_000
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.TexturePipelineRefreshInterval`.
|
||||
pub fn texture_pipeline_refresh_interval() -> f32 {
|
||||
libremetaverse_types::unimplemented_api!(
|
||||
"F:LibreMetaverse.Settings.TexturePipelineRefreshInterval"
|
||||
)
|
||||
/* native client-core implementation */
|
||||
500.0
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.UdpReceiveQueueCapacity`.
|
||||
pub fn udp_receive_queue_capacity() -> i32 {
|
||||
libremetaverse_types::unimplemented_api!(
|
||||
"F:LibreMetaverse.Settings.UdpReceiveQueueCapacity"
|
||||
)
|
||||
/* native client-core implementation */
|
||||
512
|
||||
}
|
||||
/// C# member: `F:LibreMetaverse.Settings.UserAgent`.
|
||||
pub fn user_agent() -> String {
|
||||
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Settings.UserAgent")
|
||||
/* native client-core implementation */
|
||||
"LibreMetaverse".to_owned()
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Settings.#ctor(LibreMetaverse.GridClient)`.
|
||||
pub fn new(client: libremetaverse::GridClient) -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Settings.#ctor(LibreMetaverse.GridClient)",
|
||||
)
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::Settings::defaults())
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.Agent`.
|
||||
pub fn agent(&self) -> libremetaverse::AgentSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Agent")
|
||||
/* native client-core implementation */
|
||||
self.agent.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.AssetCache`.
|
||||
pub fn asset_cache(&self) -> libremetaverse::AssetCacheSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.AssetCache")
|
||||
/* native client-core implementation */
|
||||
self.asset_cache.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.Connection`.
|
||||
pub fn connection(&self) -> libremetaverse::ConnectionSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Connection")
|
||||
/* native client-core implementation */
|
||||
self.connection.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.Logging`.
|
||||
pub fn logging(&self) -> libremetaverse::LoggingSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Logging")
|
||||
/* native client-core implementation */
|
||||
self.logging.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.Packets`.
|
||||
pub fn packets(&self) -> libremetaverse::PacketSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Packets")
|
||||
/* native client-core implementation */
|
||||
self.packets.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.Parcel`.
|
||||
pub fn parcel(&self) -> libremetaverse::ParcelSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Parcel")
|
||||
/* native client-core implementation */
|
||||
self.parcel.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.TexturePipeline`.
|
||||
pub fn texture_pipeline(&self) -> libremetaverse::TexturePipelineSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.TexturePipeline")
|
||||
/* native client-core implementation */
|
||||
self.texture_pipeline.clone()
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.Timing`.
|
||||
pub fn timing(&mut self) -> &mut libremetaverse::TimingSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.Timing")
|
||||
/* native client-core implementation */
|
||||
&mut self.timing
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.UploadCost`.
|
||||
pub fn upload_cost(&self) -> i32 {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.UploadCost")
|
||||
/* native client-core implementation */
|
||||
self.upload_cost
|
||||
}
|
||||
/// Setter for C# member: `P:LibreMetaverse.Settings.UploadCost`.
|
||||
pub fn set_upload_cost(&mut self, value: i32) {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.UploadCost")
|
||||
/* native client-core implementation */
|
||||
self.upload_cost = value
|
||||
}
|
||||
/// C# member: `P:LibreMetaverse.Settings.World`.
|
||||
pub fn world(&self) -> libremetaverse::WorldSettings {
|
||||
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Settings.World")
|
||||
/* native client-core implementation */
|
||||
self.world.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27658,6 +27691,7 @@ impl TexturePipeline {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.TexturePipelineSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TexturePipelineSettings {
|
||||
/// C# member: `F:LibreMetaverse.TexturePipelineSettings.Enabled`.
|
||||
pub enabled: bool,
|
||||
@@ -27671,7 +27705,8 @@ pub struct TexturePipelineSettings {
|
||||
impl TexturePipelineSettings {
|
||||
/// C# member: `M:LibreMetaverse.TexturePipelineSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.TexturePipelineSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::texture_pipeline_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27696,6 +27731,7 @@ pub enum TextureRequestState {
|
||||
}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.TimingSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TimingSettings {
|
||||
/// C# member: `F:LibreMetaverse.TimingSettings.AgentUpdateInterval`.
|
||||
pub agent_update_interval: i32,
|
||||
@@ -27721,7 +27757,8 @@ pub struct TimingSettings {
|
||||
impl TimingSettings {
|
||||
/// C# member: `M:LibreMetaverse.TimingSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.TimingSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::timing_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28359,6 +28396,7 @@ pub struct Vote {
|
||||
impl Vote {}
|
||||
|
||||
/// C# type: `T:LibreMetaverse.WorldSettings`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct WorldSettings {
|
||||
/// C# member: `F:LibreMetaverse.WorldSettings.AlwaysDecodeObjects`.
|
||||
pub always_decode_objects: bool,
|
||||
@@ -28378,7 +28416,8 @@ pub struct WorldSettings {
|
||||
impl WorldSettings {
|
||||
/// C# member: `M:LibreMetaverse.WorldSettings.#ctor`.
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.WorldSettings.#ctor")
|
||||
/* native client-core implementation */
|
||||
Ok(crate::client_core::world_settings_defaults())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ extern crate self as libremetaverse;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod attention_catalog;
|
||||
mod bit_pack;
|
||||
mod client_core;
|
||||
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
|
||||
mod foliage_catalog;
|
||||
mod generated;
|
||||
@@ -195,6 +196,9 @@ impl imaging::Baker {
|
||||
}
|
||||
}
|
||||
|
||||
pub use client_core::{
|
||||
ClientCoreError, ClientLifecycleState, ClientService, GridClientBuilder, ShutdownPhase,
|
||||
};
|
||||
pub use generated::*;
|
||||
pub use libremetaverse_imaging as imaging_abstractions;
|
||||
pub use libremetaverse_structured_data as structured_data;
|
||||
|
||||
Reference in New Issue
Block a user