Complete networking integration gate (#58)
All checks were successful
Native code generation / deterministic (push) Successful in 12m1s
Imaging and meshing gate / native (push) Successful in 4m3s
Native Rust workspace compile / compile (push) Successful in 4m6s

This commit is contained in:
2026-08-09 19:45:06 +00:00
parent 1fd127bd8a
commit c8b2317e13
13 changed files with 978 additions and 397 deletions

View File

@@ -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: |

View File

@@ -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.

View File

@@ -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 |

View File

@@ -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)

View File

@@ -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<Self, crate::Error> {
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<i32, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Simulator.SimStats.GetAndIncrementLastPingID",
)
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetConnectTime`.
pub fn get_connect_time(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetConnectTime")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetDroppedPackets`.
pub fn get_dropped_packets(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Simulator.SimStats.GetDroppedPackets",
)
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetIncomingBPS`.
pub fn get_incoming_bps(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetIncomingBPS")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetLastPingSent`.
pub fn get_last_ping_sent(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetLastPingSent")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetOutgoingBPS`.
pub fn get_outgoing_bps(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetOutgoingBPS")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetReceivedPongs`.
pub fn get_received_pongs(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Simulator.SimStats.GetReceivedPongs",
)
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetReceivedResends`.
pub fn get_received_resends(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Simulator.SimStats.GetReceivedResends",
)
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetRecvBytes`.
pub fn get_recv_bytes(&self) -> Result<i64, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetRecvBytes")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetRecvPackets`.
pub fn get_recv_packets(&self) -> Result<i64, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetRecvPackets")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetResentPackets`.
pub fn get_resent_packets(&self) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Simulator.SimStats.GetResentPackets",
)
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentBytes`.
pub fn get_sent_bytes(&self) -> Result<i64, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetSentBytes")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentPackets`.
pub fn get_sent_packets(&self) -> Result<i64, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SimStats.GetSentPackets")
}
/// C# member: `M:LibreMetaverse.Simulator.SimStats.GetSentPings`.
pub fn get_sent_pings(&self) -> Result<i32, crate::Error> {
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")
}
}
pub use crate::sim_stats::SimulatorSimStats;
/// C# type: `T:LibreMetaverse.SimulatorDataPool`.
pub struct SimulatorDataPool {

View File

@@ -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;

View File

@@ -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<std::time::Instant> = 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<Weak<NetworkManagerInner>>,
transport: UDPBase,
transport_thread: Mutex<Option<TransportThread>>,
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();
}
}

View File

@@ -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<i32> {
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<Self, Error> {
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<i32, Error> {
Ok(self.last_ping_id.fetch_add(1, Ordering::SeqCst))
}
pub fn get_connect_time(&self) -> Result<i32, Error> {
Ok(self.connect_time.load(Ordering::SeqCst))
}
pub fn get_dropped_packets(&self) -> Result<i32, Error> {
Ok(self.dropped_packets.load(Ordering::SeqCst))
}
pub fn get_incoming_bps(&self) -> Result<i32, Error> {
Ok(self.incoming_bps.load(Ordering::SeqCst))
}
pub fn get_last_ping_sent(&self) -> Result<i32, Error> {
Ok(self.last_ping_sent.load(Ordering::SeqCst))
}
pub fn get_outgoing_bps(&self) -> Result<i32, Error> {
Ok(self.outgoing_bps.load(Ordering::SeqCst))
}
pub fn get_received_pongs(&self) -> Result<i32, Error> {
Ok(self.received_pongs.load(Ordering::SeqCst))
}
pub fn get_received_resends(&self) -> Result<i32, Error> {
Ok(self.received_resends.load(Ordering::SeqCst))
}
pub fn get_recv_bytes(&self) -> Result<i64, Error> {
Ok(self.recv_bytes.load(Ordering::SeqCst))
}
pub fn get_recv_packets(&self) -> Result<i64, Error> {
Ok(self.recv_packets.load(Ordering::SeqCst))
}
pub fn get_resent_packets(&self) -> Result<i32, Error> {
Ok(self.resent_packets.load(Ordering::SeqCst))
}
pub fn get_sent_bytes(&self) -> Result<i64, Error> {
Ok(self.sent_bytes.load(Ordering::SeqCst))
}
pub fn get_sent_packets(&self) -> Result<i64, Error> {
Ok(self.sent_packets.load(Ordering::SeqCst))
}
pub fn get_sent_pings(&self) -> Result<i32, Error> {
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);
}
}

View File

@@ -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::<HttpRequest>::new()));

View File

@@ -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.

View File

@@ -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

106
tools/check_milestone_08.py Normal file
View File

@@ -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()

View File

@@ -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",