From 410c138013bfe90e4fba7a79ab3dade81414629f Mon Sep 17 00:00:00 2001 From: Greg Lamberson Date: Sun, 31 May 2026 17:29:57 -0500 Subject: [PATCH] feat(server): expose NetworkAutoDetect RTT via a shared handle The server measures network RTT in its AutoDetectManager, but after run() takes ownership a backend can no longer call rtt_snapshot(): the measurement is stranded in the running server. Mirror the existing display_suppressed handle so the value can be shared out. Add an Arc holding the latest measured RTT in milliseconds (u32::MAX until the first measurement), a with_autodetect_rtt_handle builder injector, and an autodetect_rtt_handle() getter. The AutoDetectRsp handler stores the measured rtt_ms into it. A display backend can then read a fresh, frame-traffic-independent network RTT for flow control without polling the server object. Drop the cfg_attr(egfx) gate on the new() too_many_arguments expect: the added parameter makes the non-egfx parameter count 8 (was 7), so the lint now fires in both feature configs and the expect is satisfied unconditionally. --- crates/ironrdp-server/src/builder.rs | 17 +++++++- crates/ironrdp-server/src/server.rs | 42 ++++++++++++++----- .../tests/server/autodetect.rs | 42 +++++++++++++++++++ 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs index fb959830c..f9c52d9d8 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -1,5 +1,5 @@ use core::net::SocketAddr; -use core::sync::atomic::AtomicBool; +use core::sync::atomic::{AtomicBool, AtomicU32}; use std::sync::Arc; use anyhow::Result; @@ -41,6 +41,7 @@ pub struct BuilderDone { #[cfg(feature = "egfx")] gfx_factory: Option>, display_suppressed: Option>, + autodetect_rtt: Option>, } pub struct RdpServerBuilder { @@ -140,6 +141,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] gfx_factory: None, display_suppressed: None, + autodetect_rtt: None, }, } } @@ -160,6 +162,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] gfx_factory: None, display_suppressed: None, + autodetect_rtt: None, }, } } @@ -241,6 +244,17 @@ impl RdpServerBuilder { self } + /// Inject a shared NetworkAutoDetect RTT handle (milliseconds, `u32::MAX` + /// until the first measurement). The server writes the latest measured RTT + /// to the same instance the backend reads. When not called, the server + /// allocates its own (still readable via + /// [`RdpServer::autodetect_rtt_handle`]). The value stays `u32::MAX` unless + /// auto-detect is enabled via [`RdpServer::enable_autodetect`]. + pub fn with_autodetect_rtt_handle(mut self, handle: Arc) -> Self { + self.state.autodetect_rtt = Some(handle); + self + } + pub fn build(self) -> RdpServer { let mut server = RdpServer::new( RdpServerOptions { @@ -257,6 +271,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] self.state.gfx_factory, self.state.display_suppressed, + self.state.autodetect_rtt, ); server.set_credential_validator(self.state.credential_validator); server diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index 0be2a0707..2e57ab105 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -1,6 +1,6 @@ use core::fmt; use core::net::SocketAddr; -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use core::time::Duration; use std::rc::Rc; use std::sync::Arc; @@ -441,6 +441,14 @@ pub struct RdpServer { /// and locks up its input dispatch for seconds on refocus while it /// chews through the backlog. display_suppressed: Arc, + + /// Latest NetworkAutoDetect round-trip time in milliseconds, or `u32::MAX` + /// until the first measurement (and while auto-detect is disabled). Updated + /// on each RTT Measure Response when auto-detect is enabled (see + /// [`Self::enable_autodetect`]). Exposed via [`Self::autodetect_rtt_handle`] + /// so display backends can read a fresh, frame-traffic-independent network + /// RTT for flow control. + autodetect_rtt: Arc, } #[derive(Debug)] @@ -475,17 +483,11 @@ enum RunState { } impl RdpServer { - // The lint only fires with the `egfx` feature on (8 args including - // `gfx_factory`); without it the parameter count is 7 and the lint - // is satisfied. `cfg_attr` keeps `#[expect]` strict in both modes. - #[cfg_attr( - feature = "egfx", - expect( - clippy::too_many_arguments, - reason = "called via the builder; positional parameters are an internal detail" - ) + #[expect( + clippy::too_many_arguments, + reason = "called via the builder; positional parameters are an internal detail" )] - pub fn new( + pub(crate) fn new( opts: RdpServerOptions, handler: Box, display: Box, @@ -494,6 +496,7 @@ impl RdpServer { connection_handler: Option>, #[cfg(feature = "egfx")] mut gfx_factory: Option>, display_suppressed: Option>, + autodetect_rtt: Option>, ) -> Self { let (ev_sender, ev_receiver) = ServerEvent::create_channel(); if let Some(cliprdr) = cliprdr_factory.as_mut() { @@ -526,6 +529,12 @@ impl RdpServer { autodetect: None, connection_handler, display_suppressed: display_suppressed.unwrap_or_else(|| Arc::new(AtomicBool::new(false))), + autodetect_rtt: { + // Reset to the sentinel: an injected handle must not expose a stale value before the first measurement. + let handle = autodetect_rtt.unwrap_or_else(|| Arc::new(AtomicU32::new(u32::MAX))); + handle.store(u32::MAX, Ordering::Relaxed); + handle + }, } } @@ -589,6 +598,16 @@ impl RdpServer { Arc::clone(&self.display_suppressed) } + /// Returns a handle to the latest NetworkAutoDetect RTT in milliseconds + /// (`u32::MAX` until the first measurement, and while auto-detect is + /// disabled). The server updates it on each RTT Measure Response; backends + /// clone the handle to read a fresh network RTT for flow control. Inject a + /// shared instance at construction with + /// [`RdpServerBuilder::with_autodetect_rtt_handle`](crate::RdpServerBuilder::with_autodetect_rtt_handle). + pub fn autodetect_rtt_handle(&self) -> Arc { + Arc::clone(&self.autodetect_rtt) + } + /// Returns the shared ECHO server handle for runtime probe requests and RTT measurements. pub fn echo_handle(&self) -> &EchoServerHandle { &self.echo_handle @@ -1408,6 +1427,7 @@ impl RdpServer { rdp::headers::ShareDataPdu::AutoDetectRsp(response) => { if let Some(ref mut ad) = self.autodetect { if let Some(rtt_ms) = ad.handle_response(&response) { + self.autodetect_rtt.store(rtt_ms, Ordering::Relaxed); debug!(rtt_ms, seq = response.sequence_number(), "RTT measured"); } else { trace!(seq = response.sequence_number(), "Unmatched auto-detect response"); diff --git a/crates/ironrdp-testsuite-core/tests/server/autodetect.rs b/crates/ironrdp-testsuite-core/tests/server/autodetect.rs index b3a3f286a..a104040e8 100644 --- a/crates/ironrdp-testsuite-core/tests/server/autodetect.rs +++ b/crates/ironrdp-testsuite-core/tests/server/autodetect.rs @@ -77,6 +77,48 @@ fn sequence_number_wraps_at_u16_max() { assert_eq!(req2.sequence_number(), 0, "should wrap around"); } +#[test] +fn autodetect_rtt_handle_defaults_to_sentinel() { + use core::net::{Ipv4Addr, SocketAddr}; + use core::sync::atomic::Ordering; + + use ironrdp_server::RdpServer; + + let server = RdpServer::builder() + .with_addr(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))) + .with_no_security() + .with_no_input() + .with_no_display() + .build(); + + assert_eq!(server.autodetect_rtt_handle().load(Ordering::Relaxed), u32::MAX); +} + +#[test] +fn with_autodetect_rtt_handle_round_trips_the_same_arc() { + use core::net::{Ipv4Addr, SocketAddr}; + use core::sync::atomic::{AtomicU32, Ordering}; + use std::sync::Arc; + + use ironrdp_server::RdpServer; + + let handle = Arc::new(AtomicU32::new(42)); + let server = RdpServer::builder() + .with_addr(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))) + .with_no_security() + .with_no_input() + .with_no_display() + .with_autodetect_rtt_handle(Arc::clone(&handle)) + .build(); + + assert!(Arc::ptr_eq(&handle, &server.autodetect_rtt_handle())); + // The server resets an injected handle to the sentinel at construction. + assert_eq!(server.autodetect_rtt_handle().load(Ordering::Relaxed), u32::MAX); + // The Arc is shared: mutating the original is visible through the server's handle. + handle.store(42, Ordering::Relaxed); + assert_eq!(server.autodetect_rtt_handle().load(Ordering::Relaxed), 42); +} + #[test] fn stale_probe_expiry() { let mut mgr = AutoDetectManager::new();