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

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