diff --git a/.gitea/workflows/codegen.yml b/.gitea/workflows/codegen.yml index 4a62a42..6b64086 100644 --- a/.gitea/workflows/codegen.yml +++ b/.gitea/workflows/codegen.yml @@ -12,6 +12,7 @@ on: - "tools/generate_*.py" - "tools/check_api_coverage.py" - "tools/check_codegen_gate.py" + - "tools/check_milestone_08.py" - "crates/libremetaverse/Cargo.toml" - "crates/libremetaverse/src/generated.rs" - "crates/libremetaverse/src/packets.rs" @@ -35,6 +36,7 @@ on: - "tools/generate_*.py" - "tools/check_api_coverage.py" - "tools/check_codegen_gate.py" + - "tools/check_milestone_08.py" - "crates/libremetaverse/Cargo.toml" - "crates/libremetaverse/src/generated.rs" - "crates/libremetaverse/src/packets.rs" @@ -66,6 +68,7 @@ jobs: python3 tools/generate_rust_mapping.py --check python3 tools/generate_api_shims.py --check python3 tools/check_api_coverage.py + python3 tools/check_milestone_08.py cargo check --manifest-path tests/api-compile/Cargo.toml --locked - name: Test generated wire codecs and translated parity cases run: | diff --git a/README.md b/README.md index 56900b6..aa65a66 100644 --- a/README.md +++ b/README.md @@ -317,3 +317,10 @@ bounded long polls preserve ack IDs, reconnect retries, shutdown `done`, and event order. Typed Linden messages share the CAPS dispatch registry; unknown messages remain observable and then use the generated caps-to-packet catalog so packet handlers receive them through the same bounded pipeline as UDP traffic. +The milestone gate now drives login, UDP, seed discovery, event-queue region +enablement, capability download, simulator handoff, and logout through one +offline fake grid. Native `Simulator.SimStats` uses atomic C#-compatible +counters and receives real UDP and server-stat updates without exposing packet +payloads or endpoints. The gate asserts token redaction, bounded policies, and +joined CAPS/download/network tasks; `tools/check_milestone_08.py` prevents owned +networking stubs or required audit evidence from regressing. diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 7d1f6fa..5752065 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | Assembly | Types | Members | Status | |---|---:|---:|---| -| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 78 types / 14,072 members; remaining surface is callable failure-only shims | +| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 79 types / 14,127 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | diff --git a/crates/libremetaverse/src/download_manager.rs b/crates/libremetaverse/src/download_manager.rs index 1877bdb..691ce97 100644 --- a/crates/libremetaverse/src/download_manager.rs +++ b/crates/libremetaverse/src/download_manager.rs @@ -319,6 +319,24 @@ impl DownloadManager { Ok(()) } + /// Number of URI-deduplicated downloads currently queued or executing. + #[must_use] + pub fn active_download_count(&self) -> usize { + mutex(&self.0.active).len() + } + + /// Whether the dispatcher has completed its idempotent teardown. + #[must_use] + pub fn is_disposed(&self) -> bool { + self.0.disposed.load(Ordering::Acquire) + } + + /// Whether a dispatcher thread is still owned by this manager. + #[must_use] + pub fn dispatcher_running(&self) -> bool { + mutex(&self.0.dispatcher).is_some() + } + #[must_use] pub fn parallel_downloads(&self) -> i32 { i32::try_from(self.0.parallel_downloads.load(Ordering::Acquire)).unwrap_or(i32::MAX) diff --git a/crates/libremetaverse/src/generated.rs b/crates/libremetaverse/src/generated.rs index fb1acde..9ba8f06 100644 --- a/crates/libremetaverse/src/generated.rs +++ b/crates/libremetaverse/src/generated.rs @@ -23945,393 +23945,62 @@ impl Simulator { } /// C# type: `T:LibreMetaverse.Simulator.SimStats`. -pub struct SimulatorSimStats; -impl SimulatorSimStats { - /// C# member: `M:LibreMetaverse.Simulator.SimStats.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.#ctor") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.AddRecvBytes(System.Int64)`. - pub fn add_recv_bytes(&self, v: i64) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.AddRecvBytes(System.Int64)", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.AddSentBytes(System.Int64)`. - pub fn add_sent_bytes(&self, v: i64) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.AddSentBytes(System.Int64)", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetAndIncrementLastPingID`. - pub fn get_and_increment_last_ping_id(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.GetAndIncrementLastPingID", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetConnectTime`. - pub fn get_connect_time(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetConnectTime") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetDroppedPackets`. - pub fn get_dropped_packets(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.GetDroppedPackets", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetIncomingBPS`. - pub fn get_incoming_bps(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetIncomingBPS") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetLastPingSent`. - pub fn get_last_ping_sent(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetLastPingSent") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetOutgoingBPS`. - pub fn get_outgoing_bps(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetOutgoingBPS") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetReceivedPongs`. - pub fn get_received_pongs(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.GetReceivedPongs", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetReceivedResends`. - pub fn get_received_resends(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.GetReceivedResends", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetRecvBytes`. - pub fn get_recv_bytes(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetRecvBytes") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetRecvPackets`. - pub fn get_recv_packets(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetRecvPackets") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetResentPackets`. - pub fn get_resent_packets(&self) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.GetResentPackets", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentBytes`. - pub fn get_sent_bytes(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetSentBytes") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentPackets`. - pub fn get_sent_packets(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetSentPackets") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentPings`. - pub fn get_sent_pings(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetSentPings") - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementDroppedPackets`. - pub fn increment_dropped_packets(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementDroppedPackets", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementReceivedPongs`. - pub fn increment_received_pongs(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementReceivedPongs", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementReceivedResends`. - pub fn increment_received_resends(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementReceivedResends", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementRecvPackets`. - pub fn increment_recv_packets(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementRecvPackets", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementResentPackets`. - pub fn increment_resent_packets(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementResentPackets", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementSentPackets`. - pub fn increment_sent_packets(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementSentPackets", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementSentPings`. - pub fn increment_sent_pings(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.IncrementSentPings", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.SetConnectTime(System.Int32)`. - pub fn set_connect_time(&self, v: i32) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.SetConnectTime(System.Int32)", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.SetIncomingBPS(System.Int32)`. - pub fn set_incoming_bps(&self, v: i32) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.SetIncomingBPS(System.Int32)", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.SetLastPingSent(System.Int32)`. - pub fn set_last_ping_sent(&self, v: i32) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.SetLastPingSent(System.Int32)", - ) - } - /// C# member: `M:LibreMetaverse.Simulator.SimStats.SetOutgoingBPS(System.Int32)`. - pub fn set_outgoing_bps(&self, v: i32) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SimStats.SetOutgoingBPS(System.Int32)", - ) - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.ActiveScripts`. - pub fn active_scripts(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.ActiveScripts" - ) - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.ActiveScripts`. - pub fn set_active_scripts(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.ActiveScripts" - ) - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.AgentTime`. - pub fn agent_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.AgentTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.AgentTime`. - pub fn set_agent_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.AgentTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.AgentUpdates`. - pub fn agent_updates(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.AgentUpdates") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.AgentUpdates`. - pub fn set_agent_updates(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.AgentUpdates") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.Agents`. - pub fn agents(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.Agents") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.Agents`. - pub fn set_agents(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.Agents") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.ChildAgents`. - pub fn child_agents(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ChildAgents") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.ChildAgents`. - pub fn set_child_agents(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ChildAgents") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.Dilation`. - pub fn dilation(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.Dilation") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.Dilation`. - pub fn set_dilation(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.Dilation") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.FPS`. - pub fn fps(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.FPS") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.FPS`. - pub fn set_fps(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.FPS") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.FrameTime`. - pub fn frame_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.FrameTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.FrameTime`. - pub fn set_frame_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.FrameTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.INPPS`. - pub fn inpps(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.INPPS") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.INPPS`. - pub fn set_inpps(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.INPPS") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.ImageTime`. - pub fn image_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ImageTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.ImageTime`. - pub fn set_image_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ImageTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.LSLIPS`. - pub fn lslips(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.LSLIPS") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.LSLIPS`. - pub fn set_lslips(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.LSLIPS") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.LastLag`. - pub fn last_lag(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.LastLag") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.LastLag`. - pub fn set_last_lag(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.LastLag") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.MissedPings`. - pub fn missed_pings(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.MissedPings") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.MissedPings`. - pub fn set_missed_pings(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.MissedPings") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.NetTime`. - pub fn net_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.NetTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.NetTime`. - pub fn set_net_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.NetTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.OUTPPS`. - pub fn outpps(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.OUTPPS") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.OUTPPS`. - pub fn set_outpps(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.OUTPPS") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.Objects`. - pub fn objects(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.Objects") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.Objects`. - pub fn set_objects(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.Objects") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.OtherTime`. - pub fn other_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.OtherTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.OtherTime`. - pub fn set_other_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.OtherTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.PendingDownloads`. - pub fn pending_downloads(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.PendingDownloads" - ) - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.PendingDownloads`. - pub fn set_pending_downloads(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.PendingDownloads" - ) - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.PendingLocalUploads`. - pub fn pending_local_uploads(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.PendingLocalUploads" - ) - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.PendingLocalUploads`. - pub fn set_pending_local_uploads(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.PendingLocalUploads" - ) - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.PendingUploads`. - pub fn pending_uploads(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.PendingUploads" - ) - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.PendingUploads`. - pub fn set_pending_uploads(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.PendingUploads" - ) - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.PhysicsFPS`. - pub fn physics_fps(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.PhysicsFPS") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.PhysicsFPS`. - pub fn set_physics_fps(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.PhysicsFPS") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.PhysicsTime`. - pub fn physics_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.PhysicsTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.PhysicsTime`. - pub fn set_physics_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.PhysicsTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.ResidentSize`. - pub fn resident_size(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ResidentSize") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.ResidentSize`. - pub fn set_resident_size(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ResidentSize") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.ScriptTime`. - pub fn script_time(&self) -> f32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ScriptTime") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.ScriptTime`. - pub fn set_script_time(&mut self, value: f32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.ScriptTime") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.ScriptedObjects`. - pub fn scripted_objects(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.ScriptedObjects" - ) - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.ScriptedObjects`. - pub fn set_scripted_objects(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Simulator.SimStats.ScriptedObjects" - ) - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.UnackedBytes`. - pub fn unacked_bytes(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.UnackedBytes") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.UnackedBytes`. - pub fn set_unacked_bytes(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.UnackedBytes") - } - /// C# member: `P:LibreMetaverse.Simulator.SimStats.VirtualSize`. - pub fn virtual_size(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.VirtualSize") - } - /// Setter for C# member: `P:LibreMetaverse.Simulator.SimStats.VirtualSize`. - pub fn set_virtual_size(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.SimStats.VirtualSize") - } -} +/// C# member: `M:LibreMetaverse.Simulator.SimStats.#ctor`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.AddRecvBytes(System.Int64)`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.AddSentBytes(System.Int64)`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetAndIncrementLastPingID`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetConnectTime`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetDroppedPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetIncomingBPS`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetLastPingSent`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetOutgoingBPS`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetReceivedPongs`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetReceivedResends`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetRecvBytes`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetRecvPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetResentPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentBytes`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentPings`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementDroppedPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementReceivedPongs`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementReceivedResends`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementRecvPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementResentPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementSentPackets`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.IncrementSentPings`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.SetConnectTime(System.Int32)`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.SetIncomingBPS(System.Int32)`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.SetLastPingSent(System.Int32)`. +/// C# member: `M:LibreMetaverse.Simulator.SimStats.SetOutgoingBPS(System.Int32)`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.ActiveScripts`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.AgentTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.AgentUpdates`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.Agents`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.ChildAgents`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.Dilation`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.FPS`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.FrameTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.INPPS`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.ImageTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.LSLIPS`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.LastLag`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.MissedPings`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.NetTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.OUTPPS`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.Objects`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.OtherTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.PendingDownloads`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.PendingLocalUploads`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.PendingUploads`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.PhysicsFPS`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.PhysicsTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.ResidentSize`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.ScriptTime`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.ScriptedObjects`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.UnackedBytes`. +/// C# member: `P:LibreMetaverse.Simulator.SimStats.VirtualSize`. +pub use crate::sim_stats::SimulatorSimStats; /// C# type: `T:LibreMetaverse.SimulatorDataPool`. pub struct SimulatorDataPool { diff --git a/crates/libremetaverse/src/lib.rs b/crates/libremetaverse/src/lib.rs index e957d77..23714c4 100644 --- a/crates/libremetaverse/src/lib.rs +++ b/crates/libremetaverse/src/lib.rs @@ -23,6 +23,7 @@ mod network_manager; #[rustfmt::skip] pub mod packet_catalog; mod packet_wire; +mod sim_stats; mod skeleton; #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. mod skeleton_catalog; diff --git a/crates/libremetaverse/src/network_manager.rs b/crates/libremetaverse/src/network_manager.rs index d66df2a..87dd190 100644 --- a/crates/libremetaverse/src/network_manager.rs +++ b/crates/libremetaverse/src/network_manager.rs @@ -16,7 +16,7 @@ use crate::packets::{ AgentPausePacket, AgentResumePacket, CloseCircuitPacket, CompleteAgentMovementPacket, CompletePingCheckPacket, GenericStreamingMessagePacket, KickUserPacket, LogoutReplyPacket, LogoutRequestPacket, Packet, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket, - StartPingCheckPacket, UseCircuitCodePacket, + SimStatsPacket, StartPingCheckPacket, UseCircuitCodePacket, }; use crate::udp_transport::{UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransportConfig}; use crate::{ @@ -35,7 +35,7 @@ use std::fmt; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicU32, AtomicU64, Ordering}; use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, sync_channel}; -use std::sync::{Arc, Condvar, Mutex, RwLock, Weak}; +use std::sync::{Arc, Condvar, Mutex, OnceLock, RwLock, Weak}; use std::thread::{self, JoinHandle}; use std::time::Duration; @@ -96,6 +96,13 @@ fn positive_millisecond_duration(value: i32) -> Duration { Duration::from_millis(u64::try_from(value.max(1)).unwrap_or(1)) } +fn environment_tick_count() -> i32 { + static START: OnceLock = OnceLock::new(); + let elapsed = START.get_or_init(std::time::Instant::now).elapsed(); + let wrapped = u32::try_from(elapsed.as_millis() & u128::from(u32::MAX)).unwrap_or_default(); + wrapped.cast_signed() +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum LogoutWaitOutcome { Reply, @@ -1011,7 +1018,6 @@ pub struct SimulatorData { manager: Mutex>, transport: UDPBase, transport_thread: Mutex>, - ping_id: AtomicU32, pause_serial: AtomicU32, } @@ -1138,7 +1144,7 @@ impl Simulator { sim_version: String::new(), size_x: size_x.unwrap_or(Self::DEFAULT_REGION_SIZE_X), size_y: size_y.unwrap_or(Self::DEFAULT_REGION_SIZE_Y), - stats: SimulatorSimStats, + stats: SimulatorSimStats::default(), terrain: Vec::new(), terrain_base0: UUID::zero(), terrain_base1: UUID::zero(), @@ -1174,7 +1180,6 @@ impl Simulator { manager: Mutex::new(Weak::new()), transport, transport_thread: Mutex::new(None), - ping_id: AtomicU32::new(0), pause_serial: AtomicU32::new(0), }); *mutex(&simulator_reference) = Arc::downgrade(&data); @@ -1265,6 +1270,7 @@ impl Simulator { } } self.connected.store(true, Ordering::Release); + self.stats.set_connect_time(environment_tick_count())?; self.handshake_complete.store(false, Ordering::Release); drop(transport_thread); self.native_use_circuit_code(true)?; @@ -1401,8 +1407,9 @@ impl Simulator { pub fn native_send_ping(&self) -> Result<(), Error> { let mut ping = StartPingCheckPacket::new_with_constructor()?; - ping.ping_id.ping_id = self.ping_id.fetch_add(1, Ordering::Relaxed).to_le_bytes()[0]; + ping.ping_id.ping_id = self.stats.get_and_increment_last_ping_id()?.to_le_bytes()[0]; ping.ping_id.oldest_unacked = 0; + self.stats.set_last_ping_sent(environment_tick_count())?; let data = ping.to_bytes_with_method()?; self.native_send_packet_data( data.clone(), @@ -1658,6 +1665,16 @@ impl UdpPacketHandler for SimulatorUdpHandler { else { return; }; + let _ = simulator + .stats + .add_recv_bytes(i64::from(buffer.data_length)); + let _ = simulator.stats.increment_recv_packets(); + if payload + .first() + .is_some_and(|flags| flags & Helpers::MSG_RESENT != 0) + { + let _ = simulator.stats.increment_received_resends(); + } simulator.note_packet_received(packet.type_); let manager = mutex(&simulator.manager).upgrade(); if let Some(manager) = manager { @@ -1674,11 +1691,29 @@ impl UdpPacketHandler for SimulatorUdpHandler { return; }; let simulator = Simulator { caps: None, data }; + let _ = simulator + .stats + .add_sent_bytes(i64::try_from(bytes_sent).unwrap_or(i64::MAX)); + let _ = simulator.stats.increment_sent_packets(); + if buffer + .data + .first() + .is_some_and(|flags| flags & Helpers::MSG_RESENT != 0) + { + let _ = simulator.stats.increment_resent_packets(); + } let manager = mutex(&simulator.manager).upgrade(); if let Some(manager) = manager { manager.raise_packet_sent(buffer.data, bytes_sent, simulator); } } + + fn packet_dropped(&self) { + let Some(data) = mutex(&self.simulator).upgrade() else { + return; + }; + let _ = data.stats.increment_dropped_packets(); + } } #[derive(Clone)] @@ -1929,6 +1964,9 @@ impl NetworkManagerInner { simulator.handshake_complete.store(true, Ordering::Release); simulator.handshake_wait.notify_all(); } + PacketType::CompletePingCheck => { + simulator.stats.record_pong(environment_tick_count()); + } PacketType::StartPingCheck => { let Some(raw_data) = raw_data else { return; @@ -2022,6 +2060,40 @@ impl NetworkManagerInner { "ClientInitiated".to_owned(), ); } + PacketType::SimStats => { + if !self.client.settings_ref().packets.enable_sim_stats { + return; + } + let Some(raw_data) = raw_data else { + return; + }; + let Ok(mut incoming) = SimStatsPacket::new_with_constructor() else { + return; + }; + let Ok(mut packet_end) = i32::try_from(raw_data.len()) else { + return; + }; + packet_end -= 1; + let mut position = 0; + let mut zero_buffer = + vec![0_u8; UdpTransportConfig::default().max_decoded_packet_size]; + if incoming + .from_bytes_with_bytes_int32_int32_bytes( + raw_data.to_vec(), + &mut position, + &mut packet_end, + Some(&mut zero_buffer), + ) + .is_err() + { + return; + } + for stat in incoming.stat { + simulator + .stats + .apply_server_stat(stat.stat_id, stat.stat_value); + } + } PacketType::DisableSimulator => { let _ = self.disconnect_simulator(simulator.clone(), false); } @@ -2196,8 +2268,8 @@ impl NetworkManagerInner { crate::NetworkManagerDisconnectType::NetworkTimeout, "NetworkTimeout".to_owned(), ); - } else { - let _ = current.native_send_ping(); + } else if current.native_send_ping().is_ok() { + let _ = current.stats.increment_sent_pings(); } } diff --git a/crates/libremetaverse/src/sim_stats.rs b/crates/libremetaverse/src/sim_stats.rs new file mode 100644 index 0000000..4c5cfe4 --- /dev/null +++ b/crates/libremetaverse/src/sim_stats.rs @@ -0,0 +1,347 @@ +//! Thread-safe simulator statistics matching `Simulator.SimStats`. + +#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map. + +use crate::Error; +use std::fmt; +use std::sync::atomic::{AtomicI32, AtomicI64, AtomicU32, Ordering}; + +fn csharp_convert_to_i32(value: f32) -> Option { + let rounded = value.round_ties_even(); + if !rounded.is_finite() || !(-2_147_483_648.0..2_147_483_648.0).contains(&rounded) { + return None; + } + #[allow(clippy::cast_possible_truncation)] + Some(rounded as i32) +} + +macro_rules! integer_property { + ($get:ident, $set:ident, $field:ident) => { + #[must_use] + pub fn $get(&self) -> i32 { + self.$field.load(Ordering::Acquire) + } + + pub fn $set(&mut self, value: i32) { + self.$field.store(value, Ordering::Release); + } + }; +} + +macro_rules! float_property { + ($get:ident, $set:ident, $field:ident) => { + #[must_use] + pub fn $get(&self) -> f32 { + f32::from_bits(self.$field.load(Ordering::Acquire)) + } + + pub fn $set(&mut self, value: f32) { + self.$field.store(value.to_bits(), Ordering::Release); + } + }; +} + +#[derive(Default)] +pub struct SimulatorSimStats { + sent_packets: AtomicI64, + recv_packets: AtomicI64, + sent_bytes: AtomicI64, + recv_bytes: AtomicI64, + connect_time: AtomicI32, + resent_packets: AtomicI32, + received_resends: AtomicI32, + dropped_packets: AtomicI32, + sent_pings: AtomicI32, + received_pongs: AtomicI32, + incoming_bps: AtomicI32, + outgoing_bps: AtomicI32, + last_ping_sent: AtomicI32, + last_ping_id: AtomicI32, + last_lag: AtomicI32, + missed_pings: AtomicI32, + dilation: AtomicU32, + fps: AtomicI32, + physics_fps: AtomicU32, + agent_updates: AtomicU32, + frame_time: AtomicU32, + net_time: AtomicU32, + physics_time: AtomicU32, + image_time: AtomicU32, + script_time: AtomicU32, + agent_time: AtomicU32, + other_time: AtomicU32, + objects: AtomicI32, + scripted_objects: AtomicI32, + agents: AtomicI32, + child_agents: AtomicI32, + active_scripts: AtomicI32, + lslips: AtomicI32, + inpps: AtomicI32, + outpps: AtomicI32, + pending_downloads: AtomicI32, + pending_uploads: AtomicI32, + virtual_size: AtomicI32, + resident_size: AtomicI32, + pending_local_uploads: AtomicI32, + unacked_bytes: AtomicI32, +} + +impl fmt::Debug for SimulatorSimStats { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("SimulatorSimStats") + .field("sent_packets", &self.sent_packets.load(Ordering::Relaxed)) + .field("recv_packets", &self.recv_packets.load(Ordering::Relaxed)) + .field("sent_bytes", &self.sent_bytes.load(Ordering::Relaxed)) + .field("recv_bytes", &self.recv_bytes.load(Ordering::Relaxed)) + .field( + "dropped_packets", + &self.dropped_packets.load(Ordering::Relaxed), + ) + .finish_non_exhaustive() + } +} + +impl SimulatorSimStats { + pub fn new() -> Result { + Ok(Self::default()) + } + + pub fn add_recv_bytes(&self, value: i64) -> Result<(), Error> { + self.recv_bytes.fetch_add(value, Ordering::SeqCst); + Ok(()) + } + + pub fn add_sent_bytes(&self, value: i64) -> Result<(), Error> { + self.sent_bytes.fetch_add(value, Ordering::SeqCst); + Ok(()) + } + + pub fn get_and_increment_last_ping_id(&self) -> Result { + Ok(self.last_ping_id.fetch_add(1, Ordering::SeqCst)) + } + + pub fn get_connect_time(&self) -> Result { + Ok(self.connect_time.load(Ordering::SeqCst)) + } + + pub fn get_dropped_packets(&self) -> Result { + Ok(self.dropped_packets.load(Ordering::SeqCst)) + } + + pub fn get_incoming_bps(&self) -> Result { + Ok(self.incoming_bps.load(Ordering::SeqCst)) + } + + pub fn get_last_ping_sent(&self) -> Result { + Ok(self.last_ping_sent.load(Ordering::SeqCst)) + } + + pub fn get_outgoing_bps(&self) -> Result { + Ok(self.outgoing_bps.load(Ordering::SeqCst)) + } + + pub fn get_received_pongs(&self) -> Result { + Ok(self.received_pongs.load(Ordering::SeqCst)) + } + + pub fn get_received_resends(&self) -> Result { + Ok(self.received_resends.load(Ordering::SeqCst)) + } + + pub fn get_recv_bytes(&self) -> Result { + Ok(self.recv_bytes.load(Ordering::SeqCst)) + } + + pub fn get_recv_packets(&self) -> Result { + Ok(self.recv_packets.load(Ordering::SeqCst)) + } + + pub fn get_resent_packets(&self) -> Result { + Ok(self.resent_packets.load(Ordering::SeqCst)) + } + + pub fn get_sent_bytes(&self) -> Result { + Ok(self.sent_bytes.load(Ordering::SeqCst)) + } + + pub fn get_sent_packets(&self) -> Result { + Ok(self.sent_packets.load(Ordering::SeqCst)) + } + + pub fn get_sent_pings(&self) -> Result { + Ok(self.sent_pings.load(Ordering::SeqCst)) + } + + pub fn increment_dropped_packets(&self) -> Result<(), Error> { + self.dropped_packets.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn increment_received_pongs(&self) -> Result<(), Error> { + self.received_pongs.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn increment_received_resends(&self) -> Result<(), Error> { + self.received_resends.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn increment_recv_packets(&self) -> Result<(), Error> { + self.recv_packets.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn increment_resent_packets(&self) -> Result<(), Error> { + self.resent_packets.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn increment_sent_packets(&self) -> Result<(), Error> { + self.sent_packets.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn increment_sent_pings(&self) -> Result<(), Error> { + self.sent_pings.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + pub fn set_connect_time(&self, value: i32) -> Result<(), Error> { + self.connect_time.store(value, Ordering::SeqCst); + Ok(()) + } + + pub fn set_incoming_bps(&self, value: i32) -> Result<(), Error> { + self.incoming_bps.store(value, Ordering::SeqCst); + Ok(()) + } + + pub fn set_last_ping_sent(&self, value: i32) -> Result<(), Error> { + self.last_ping_sent.store(value, Ordering::SeqCst); + Ok(()) + } + + pub fn set_outgoing_bps(&self, value: i32) -> Result<(), Error> { + self.outgoing_bps.store(value, Ordering::SeqCst); + Ok(()) + } + + pub(crate) fn record_pong(&self, tick_count: i32) { + let sent = self.last_ping_sent.load(Ordering::Acquire); + self.last_lag + .store(tick_count.wrapping_sub(sent), Ordering::Release); + self.received_pongs.fetch_add(1, Ordering::SeqCst); + } + + pub(crate) fn apply_server_stat(&self, id: u32, value: f32) { + let float = |field: &AtomicU32| field.store(value.to_bits(), Ordering::Release); + let integer = |field: &AtomicI32| { + if let Some(value) = csharp_convert_to_i32(value) { + field.store(value, Ordering::Release); + } + }; + match id { + 0 => float(&self.dilation), + 1 => integer(&self.fps), + 2 => float(&self.physics_fps), + 3 => float(&self.agent_updates), + 4 => float(&self.frame_time), + 5 => float(&self.net_time), + 6 => float(&self.other_time), + 7 => float(&self.physics_time), + 8 => float(&self.agent_time), + 9 => float(&self.image_time), + 10 => float(&self.script_time), + 11 => integer(&self.objects), + 12 => integer(&self.scripted_objects), + 13 => integer(&self.agents), + 14 => integer(&self.child_agents), + 15 => integer(&self.active_scripts), + 16 => integer(&self.lslips), + 17 => integer(&self.inpps), + 18 => integer(&self.outpps), + 19 => integer(&self.pending_downloads), + 20 => integer(&self.pending_uploads), + 21 => integer(&self.virtual_size), + 22 => integer(&self.resident_size), + 23 => integer(&self.pending_local_uploads), + 24 => integer(&self.unacked_bytes), + _ => {} + } + } + + integer_property!(active_scripts, set_active_scripts, active_scripts); + float_property!(agent_time, set_agent_time, agent_time); + float_property!(agent_updates, set_agent_updates, agent_updates); + integer_property!(agents, set_agents, agents); + integer_property!(child_agents, set_child_agents, child_agents); + float_property!(dilation, set_dilation, dilation); + integer_property!(fps, set_fps, fps); + float_property!(frame_time, set_frame_time, frame_time); + integer_property!(inpps, set_inpps, inpps); + float_property!(image_time, set_image_time, image_time); + integer_property!(lslips, set_lslips, lslips); + integer_property!(last_lag, set_last_lag, last_lag); + integer_property!(missed_pings, set_missed_pings, missed_pings); + float_property!(net_time, set_net_time, net_time); + integer_property!(outpps, set_outpps, outpps); + integer_property!(objects, set_objects, objects); + float_property!(other_time, set_other_time, other_time); + integer_property!(pending_downloads, set_pending_downloads, pending_downloads); + integer_property!( + pending_local_uploads, + set_pending_local_uploads, + pending_local_uploads + ); + integer_property!(pending_uploads, set_pending_uploads, pending_uploads); + float_property!(physics_fps, set_physics_fps, physics_fps); + float_property!(physics_time, set_physics_time, physics_time); + integer_property!(resident_size, set_resident_size, resident_size); + float_property!(script_time, set_script_time, script_time); + integer_property!(scripted_objects, set_scripted_objects, scripted_objects); + integer_property!(unacked_bytes, set_unacked_bytes, unacked_bytes); + integer_property!(virtual_size, set_virtual_size, virtual_size); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::thread; + + #[test] + fn counters_are_atomic_and_properties_preserve_float_bits() { + let stats = Arc::new(SimulatorSimStats::new().unwrap()); + let workers: Vec<_> = (0..4) + .map(|_| { + let stats = Arc::clone(&stats); + thread::spawn(move || { + for _ in 0..1_000 { + stats.increment_recv_packets().unwrap(); + stats.add_recv_bytes(7).unwrap(); + } + }) + }) + .collect(); + for worker in workers { + worker.join().unwrap(); + } + assert_eq!(stats.get_recv_packets().unwrap(), 4_000); + assert_eq!(stats.get_recv_bytes().unwrap(), 28_000); + assert_eq!(stats.get_and_increment_last_ping_id().unwrap(), 0); + assert_eq!(stats.get_and_increment_last_ping_id().unwrap(), 1); + + let mut stats = Arc::try_unwrap(stats).ok().unwrap(); + stats.set_dilation(f32::from_bits(0x7fc0_1234)); + assert_eq!(stats.dilation().to_bits(), 0x7fc0_1234); + + stats.apply_server_stat(1, 44.5); + assert_eq!(stats.fps(), 44); + stats.apply_server_stat(1, 45.5); + assert_eq!(stats.fps(), 46); + stats.apply_server_stat(1, f32::NAN); + assert_eq!(stats.fps(), 46); + } +} diff --git a/crates/libremetaverse/tests/network_manager.rs b/crates/libremetaverse/tests/network_manager.rs index 2122bd1..f2c7fbf 100644 --- a/crates/libremetaverse/tests/network_manager.rs +++ b/crates/libremetaverse/tests/network_manager.rs @@ -1,3 +1,4 @@ +use libremetaverse::http::DownloadManager; use libremetaverse::interfaces::IMessage; use libremetaverse::messages::linden::{ EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock, @@ -6,7 +7,7 @@ use libremetaverse::packets::{ CompleteAgentMovementPacket, DisableSimulatorPacket, LogoutReplyPacket, LogoutReplyPacketInventoryDataBlock, LogoutRequestPacket, Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket, - StartPingCheckPacket, UseCircuitCodePacket, + SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket, }; use libremetaverse::{ CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient, LoginState, @@ -363,6 +364,7 @@ fn sim_connecting_can_cancel_without_leaking_collection_state() { enum ServerCommand { Ping, + Stats, Disable, ExpectEconomy, Handoff, @@ -518,6 +520,26 @@ fn spawn_fake_server() -> FakeServer { ServerCommand::Ping => { serve_ping_command(&socket, client_endpoint, &report_sender, &mut buffer); } + ServerCommand::Stats => { + let mut stats = SimStatsPacket::new_with_constructor().unwrap(); + stats.stat = vec![ + SimStatsPacketStatBlock { + stat_id: 0, + stat_value: 0.75, + }, + SimStatsPacketStatBlock { + stat_id: 1, + stat_value: 44.5, + }, + SimStatsPacketStatBlock { + stat_id: 11, + stat_value: 321.0, + }, + ]; + let mut bytes = stats.to_bytes_with_method().unwrap(); + bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); + socket.send_to(&bytes, client_endpoint).unwrap(); + } ServerCommand::Disable => { let disable = DisableSimulatorPacket::new_with_constructor().unwrap(); let mut bytes = disable.to_bytes_with_method().unwrap(); @@ -770,6 +792,15 @@ fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() { assert_reuse_preserves_connected_override(&mut manager, &server, &sim); assert_ping_exchange(&server, &packet_receiver); + server.commands.send(ServerCommand::Stats).unwrap(); + wait_until(|| sim.stats.objects() == 321); + assert_eq!(sim.stats.dilation(), 0.75); + assert_eq!(sim.stats.fps(), 44); + assert!(sim.stats.get_recv_packets().unwrap() >= 3); + assert!(sim.stats.get_recv_bytes().unwrap() > 0); + assert!(sim.stats.get_sent_packets().unwrap() >= 3); + assert!(sim.stats.get_sent_bytes().unwrap() > 0); + assert_eq!(sim.stats.get_dropped_packets().unwrap(), 0); server.commands.send(ServerCommand::Disable).unwrap(); assert_eq!( @@ -1681,6 +1712,308 @@ async fn login_hashes_credentials_parses_response_and_hands_off_session_to_udp_a .unwrap(); } +#[tokio::test(flavor = "current_thread")] +async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task_leaks() { + let _network_guard = network_test_guard(); + let first_server = spawn_fake_server(); + let second_server = spawn_fake_server(); + + let mut enable = EnableSimulatorMessage::new().unwrap(); + let mut simulator_info = EnableSimulatorMessageSimulatorInfoBlock::new().unwrap(); + simulator_info.ip = second_server.endpoint.ip(); + simulator_info.port = i32::from(second_server.endpoint.port()); + simulator_info.region_handle = 0x2000; + simulator_info.region_size_x = 512; + simulator_info.region_size_y = 256; + enable.simulators = vec![simulator_info]; + let enable_body = enable.serialize().unwrap().snapshot(); + + let release_seed = Arc::new(AtomicBool::new(false)); + let release_queue = Arc::new(AtomicBool::new(false)); + let queue_calls = Arc::new(AtomicUsize::new(0)); + let queue_cancelled = Arc::new(AtomicBool::new(false)); + let queue_done = Arc::new(AtomicBool::new(false)); + let request_uris = Arc::new(Mutex::new(Vec::new())); + let handler = HttpMessageHandler::new({ + let release_seed = Arc::clone(&release_seed); + let release_queue = Arc::clone(&release_queue); + let queue_calls = Arc::clone(&queue_calls); + let queue_cancelled = Arc::clone(&queue_cancelled); + let queue_done = Arc::clone(&queue_done); + let request_uris = Arc::clone(&request_uris); + let first_endpoint = first_server.endpoint; + move |request, cancellation| { + let release_seed = Arc::clone(&release_seed); + let release_queue = Arc::clone(&release_queue); + let queue_calls = Arc::clone(&queue_calls); + let queue_cancelled = Arc::clone(&queue_cancelled); + let queue_done = Arc::clone(&queue_done); + let request_uris = Arc::clone(&request_uris); + let enable_body = enable_body.clone(); + async move { + let uri = request.uri.0.clone(); + request_uris.lock().unwrap().push(uri.clone()); + if uri.contains("/login") { + return login_http_response(successful_login_response( + first_endpoint, + "https://fake.grid/seed?token=seed-secret", + )); + } + if uri.contains("/seed") { + while !release_seed.load(Ordering::Acquire) { + tokio::time::sleep(Duration::from_millis(1)).await; + } + return login_http_response(OSD::Map(HashMap::from([ + ( + "EventQueueGet".into(), + OSD::Uri(Uri("https://fake.grid/queue?token=queue-secret".into())), + ), + ( + "GetTexture".into(), + OSD::Uri(Uri("https://fake.grid/asset?token=asset-secret".into())), + ), + ]))); + } + if uri.contains("/queue") { + let done = matches!( + OSDParser::deserialize_llsd_xml_with_bytes(request.body.clone()), + Ok(OSD::Map(ref map)) + if map + .get("done") + .and_then(|value| value.as_boolean().ok()) + == Some(true) + ); + if done { + queue_done.store(true, Ordering::Release); + return HttpResponse { + status_code: 499, + headers: BTreeMap::new(), + content_type: Some(HttpCapsClient::LLSD_XML.into()), + body: Vec::new(), + }; + } + let call = queue_calls.fetch_add(1, Ordering::AcqRel); + if call == 0 { + while !release_queue.load(Ordering::Acquire) { + tokio::time::sleep(Duration::from_millis(1)).await; + } + return login_http_response(OSD::Map(HashMap::from([ + ("id".into(), OSD::Integer(1)), + ( + "events".into(), + OSD::Array(vec![OSD::Map(HashMap::from([ + ("message".into(), OSD::String("EnableSimulator".into())), + ("body".into(), OSD::Map(enable_body)), + ]))]), + ), + ]))); + } + let observed_cancellation = Arc::clone(&queue_cancelled); + let _cancellation_guard = cancellation.register_callback(Arc::new(move || { + observed_cancellation.store(true, Ordering::Release); + })); + cancellation.cancelled().await; + return HttpResponse { + status_code: 499, + headers: BTreeMap::new(), + content_type: Some(HttpCapsClient::LLSD_XML.into()), + body: Vec::new(), + }; + } + if uri.contains("/asset") { + return HttpResponse { + status_code: 200, + headers: BTreeMap::new(), + content_type: Some("application/octet-stream".into()), + body: b"fake-grid-asset".to_vec(), + }; + } + HttpResponse { + status_code: 404, + headers: BTreeMap::new(), + content_type: None, + body: Vec::new(), + } + } + } + }); + + let mut client = GridClient::new().unwrap(); + client.settings().timing().login_timeout = 2_000; + client.settings().agent_settings_mut().multiple_sims = true; + client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); + let manager = client.network(); + let (queue_sender, queue_receiver) = channel(); + let _queue_guard = manager.subscribe_event_queue_running(Arc::new(move |args| { + queue_sender.send(args.simulator().handle).unwrap(); + })); + let (logout_sender, logout_receiver) = channel(); + let _logout_guard = manager.subscribe_logged_out(Arc::new(move |args| { + logout_sender.send(args.inventory_items).unwrap(); + })); + + let mut params = manager + .default_login_params( + "Ada".into(), + "Lovelace".into(), + "plain-secret".into(), + "MetaCrate".into(), + "1.0".into(), + ) + .unwrap(); + params.uri = "https://fake.grid/login".into(); + assert!( + manager + .login_with_login_params_cancellation_token(params, None) + .await + .unwrap() + ); + assert_eq!( + first_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::Circuit(0x1234) + ); + assert_eq!( + first_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) + ); + let first = manager.current_sim().expect("initial simulator"); + let caps = first.clone().caps.expect("seed capability client"); + release_seed.store(true, Ordering::Release); + wait_until(|| { + caps.capability_uri("GetTexture".into()) + .ok() + .flatten() + .is_some() + }); + release_queue.store(true, Ordering::Release); + assert_eq!( + queue_receiver.recv_timeout(Duration::from_secs(2)).unwrap(), + first.handle + ); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(2)) + .unwrap(), + ServerReport::Circuit(0x1234) + ); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) + ); + let second = manager + .find_simulator_with_ip_end_point(second_server.endpoint) + .unwrap() + .expect("event-queue simulator"); + + let downloads = DownloadManager::new(client.clone()).unwrap(); + let (_, asset) = downloads + .queue_download_with_uri_string_i_progress_cancellation_token_int32( + caps.capability_uri("GetTexture".into()).unwrap().unwrap(), + None, + None, + None, + Some(0), + ) + .await + .unwrap(); + assert_eq!(asset, b"fake-grid-asset"); + assert_eq!(downloads.active_download_count(), 0); + + second_server.commands.send(ServerCommand::Handoff).unwrap(); + manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + second_server.endpoint, + second.handle, + true, + None, + second.size_x, + second.size_y, + ) + .unwrap(); + assert_eq!(manager.current_sim(), Some(second.clone())); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::Circuit(0x1234) + ); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::Movement(0x1234) + ); + + let diagnostics = format!("{manager:?} {caps:?} {downloads:?} {first:?} {second:?}"); + for secret in [ + "seed-secret", + "queue-secret", + "asset-secret", + "plain-secret", + ] { + assert!(!diagnostics.contains(secret)); + } + assert!(first.stats.get_recv_packets().unwrap() > 0); + assert!(first.stats.get_sent_packets().unwrap() > 0); + assert!(second.stats.get_recv_packets().unwrap() > 0); + assert!(second.stats.get_sent_packets().unwrap() > 0); + + wait_until(|| queue_calls.load(Ordering::Acquire) >= 2); + let returned_inventory = + vec![UUID::new_with_string("33333333-3333-3333-3333-333333333333".into()).unwrap()]; + second_server + .commands + .send(ServerCommand::LogoutReply(returned_inventory.clone())) + .unwrap(); + manager.logout_with_method().unwrap(); + assert_eq!( + logout_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + returned_inventory + ); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::Logout( + UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap(), + UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap(), + ) + ); + wait_until(|| queue_cancelled.load(Ordering::Acquire)); + assert!(!queue_done.load(Ordering::Acquire)); + assert!(!caps.is_event_queue_running()); + assert!(!manager.connected()); + assert!(manager.simulators.is_empty()); + assert_eq!(manager.pending_logout_tasks(), 0); + assert!(!first.connected()); + assert!(!second.connected()); + + downloads.dispose().unwrap(); + assert!(downloads.is_disposed()); + assert!(!downloads.dispatcher_running()); + assert_eq!(downloads.active_download_count(), 0); + let uris = request_uris.lock().unwrap(); + assert!(uris.iter().any(|uri| uri.contains("/login"))); + assert!(uris.iter().any(|uri| uri.contains("/seed"))); + assert!(uris.iter().any(|uri| uri.contains("/queue"))); + assert!(uris.iter().any(|uri| uri.contains("/asset"))); +} + #[tokio::test(flavor = "current_thread")] async fn redirect_reuses_hashed_credentials_and_failure_is_typed_without_partial_session() { let requests = Arc::new(Mutex::new(Vec::::new())); diff --git a/docs/caps-http.md b/docs/caps-http.md index e708628..ae82256 100644 --- a/docs/caps-http.md +++ b/docs/caps-http.md @@ -89,6 +89,10 @@ statuses and transport failures retry up to the request's configured retry count with bounded backoff. Disposal cancels active and queued work, wakes every waiter, joins the dispatcher, and prevents new requests. +`DownloadManager::active_download_count`, `dispatcher_running`, and +`is_disposed` expose payload-free lifecycle state for leak assertions. They do +not expose capability URIs or response data. + Errors expose only typed categories and `Debug` output substitutes a redacted marker for capability URIs. No request URL, query token, body, or response body is logged. diff --git a/docs/network-manager.md b/docs/network-manager.md index 09d6c19..bfd95e9 100644 --- a/docs/network-manager.md +++ b/docs/network-manager.md @@ -113,6 +113,26 @@ canceled login leaves no simulator installed. Passwords, login tokens, MFA hashes, session identifiers, seed capabilities, and estate access tokens are excluded from `Debug` output and diagnostic messages. +## Statistics and milestone gate + +`SimulatorSimStats` is the native thread-safe counterpart of the reference +`Simulator.SimStats`. Atomic counters track received/sent packets and bytes, +resends, drops, pings, pongs, lag, and bandwidth samples. Server `SimStats` +blocks update the 25 defined simulator measurements with the reference ID and +rounding rules; unknown IDs are ignored. Debug snapshots contain aggregate +counters only—never packet bytes, endpoints, capability URLs, or credentials. + +The offline fake-grid gate composes the completed layers rather than replacing +their focused tests. It authenticates through injected LLSD HTTP, establishes a +real loopback UDP circuit, discovers seed capabilities, consumes an +`EventQueueGet` message that enables a second region, downloads a capability +asset, promotes that region, and completes the logout handshake. It then proves +CAPS polling was canceled, download and network workers were joined, sockets +were closed, state was cleared, and diagnostic formatting contains none of the +fixture tokens. `tools/check_milestone_08.py` also audits owned source files for +failure-only bodies and verifies the explicit queue, payload, event, retry, and +concurrency policy markers remain present. + The deterministic tests use loopback fake simulators and require no live grid: ```sh diff --git a/tools/check_milestone_08.py b/tools/check_milestone_08.py new file mode 100644 index 0000000..c93b248 --- /dev/null +++ b/tools/check_milestone_08.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Audit the native networking/client lifecycle milestone boundary.""" + +from __future__ import annotations + +from pathlib import Path +import re + +import generate_api_shims + + +ROOT = Path(__file__).resolve().parents[1] +OWNED_SOURCES = ( + "caps.rs", + "caps_http.rs", + "client_core.rs", + "download_manager.rs", + "event_queue.rs", + "login.rs", + "network_manager.rs", + "sim_stats.rs", + "udp_transport.rs", +) +REQUIRED_NATIVE_TYPES = { + "T:LibreMetaverse.Caps", + "T:LibreMetaverse.GridClient", + "T:LibreMetaverse.Http.DownloadManager", + "T:LibreMetaverse.Http.EventQueueClient", + "T:LibreMetaverse.HttpCapsClient", + "T:LibreMetaverse.NetworkManager", + "T:LibreMetaverse.Settings", + "T:LibreMetaverse.Simulator", + "T:LibreMetaverse.Simulator.SimStats", + "T:LibreMetaverse.UDPBase", + "T:LibreMetaverse.UDPPacketBuffer", +} +STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") + + +def check_owned_sources() -> None: + source_root = ROOT / "crates" / "libremetaverse" / "src" + offenders = [ + name for name in OWNED_SOURCES if STUB_RE.search((source_root / name).read_text()) + ] + if offenders: + raise SystemExit("milestone 08 owned Rust stubs remain: " + ", ".join(offenders)) + + +def check_native_mapping() -> None: + native = generate_api_shims.NATIVE_TYPES.keys() | generate_api_shims.NATIVE_DECLARATIONS.keys() + missing = sorted(REQUIRED_NATIVE_TYPES - native) + if missing: + raise SystemExit("milestone 08 native type mappings missing: " + ", ".join(missing)) + + +def check_fake_grid_gate() -> None: + tests = (ROOT / "crates" / "libremetaverse" / "tests" / "network_manager.rs").read_text() + required_evidence = ( + "offline_fake_grid_drives_the_complete_networking_lifecycle_without_task_leaks", + "EventQueueGet", + "active_download_count", + "dispatcher_running", + "get_recv_packets", + "get_sent_packets", + "seed-secret", + "queue-secret", + "asset-secret", + ) + missing = [marker for marker in required_evidence if marker not in tests] + if missing: + raise SystemExit("fake-grid audit evidence missing: " + ", ".join(missing)) + + +def check_bounded_policies() -> None: + expectations = { + "caps_http.rs": ("CapsHttpLimits", "max_redirects", "max_response_bytes"), + "download_manager.rs": ("DOWNLOAD_QUEUE_CAPACITY", "MAX_PARALLEL_DOWNLOADS"), + "event_queue.rs": ("EventQueuePolicy", "max_events_per_response", "max_event_nodes"), + "udp_transport.rs": ( + "UdpTransportConfig", + "receive_queue_capacity", + "command_queue_capacity", + "write_queue_capacity", + ), + } + source_root = ROOT / "crates" / "libremetaverse" / "src" + for name, markers in expectations.items(): + source = (source_root / name).read_text() + missing = [marker for marker in markers if marker not in source] + if missing: + raise SystemExit(f"{name}: bounded policy evidence missing: " + ", ".join(missing)) + + +def main() -> None: + check_owned_sources() + check_native_mapping() + check_fake_grid_gate() + check_bounded_policies() + print( + "milestone 08 audit: native networking sources contain no stubs; " + "fake-grid lifecycle, redaction, task cleanup, statistics, and bounded policies are present" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index b533e3e..253e077 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -75,6 +75,7 @@ NATIVE_TYPES = { "T:LibreMetaverse.SimConnectedEventArgs": "crate::network_manager::SimConnectedEventArgs", "T:LibreMetaverse.SimConnectingEventArgs": "crate::network_manager::SimConnectingEventArgs", "T:LibreMetaverse.SimDisconnectedEventArgs": "crate::network_manager::SimDisconnectedEventArgs", + "T:LibreMetaverse.Simulator.SimStats": "crate::sim_stats::SimulatorSimStats", "T:LibreMetaverse.Messages.Linden.EventMessageBlock": "crate::event_queue::EventMessageBlock", "T:LibreMetaverse.Messages.Linden.EventQueueAck": "crate::event_queue::EventQueueAck", "T:LibreMetaverse.Messages.Linden.EventQueueEvent": "crate::event_queue::EventQueueEvent",