1use crate::client::config_management::Credential;
6use crate::client::connection_selection::ConnectionSelectionRequester;
7use crate::client::roaming::lib::*;
8use crate::client::types;
9use crate::telemetry::{TelemetryEvent, TelemetrySender};
10use anyhow::{Error, format_err};
11use async_trait::async_trait;
12use futures::channel::mpsc;
13use futures::future::LocalBoxFuture;
14use futures::lock::Mutex;
15use futures::stream::{FuturesUnordered, StreamExt};
16use futures::{FutureExt, select};
17use log::{debug, error, info, warn};
18use std::any::Any;
19use std::sync::Arc;
20use {fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_internal as fidl_internal};
21
22pub mod default_monitor;
23pub mod stationary_monitor;
24
25pub struct RoamDataSender {
27 sender: mpsc::Sender<RoamTriggerData>,
28}
29impl RoamDataSender {
30 pub fn new(trigger_data_sender: mpsc::Sender<RoamTriggerData>) -> Self {
31 Self { sender: trigger_data_sender }
32 }
33 pub fn send_signal_report_ind(
34 &mut self,
35 ind: fidl_internal::SignalReportIndication,
36 ) -> Result<(), anyhow::Error> {
37 Ok(self.sender.try_send(RoamTriggerData::SignalReportInd(ind))?)
38 }
39}
40#[async_trait(?Send)]
42pub trait RoamMonitorApi: Any {
43 async fn handle_roam_trigger_data(
47 &mut self,
48 data: RoamTriggerData,
49 ) -> Result<RoamTriggerDataOutcome, anyhow::Error>;
50 fn should_send_roam_request(&self, request: PolicyRoamRequest) -> Result<bool, anyhow::Error>;
53 fn notify_of_roam_attempt(&mut self);
56}
57
58pub async fn serve_roam_monitor(
61 mut roam_monitor: Box<dyn RoamMonitorApi>,
62 roaming_policy: RoamingPolicy,
63 mut trigger_data_receiver: mpsc::Receiver<RoamTriggerData>,
64 connection_selection_requester: ConnectionSelectionRequester,
65 mut roam_request_sender: mpsc::Sender<PolicyRoamRequest>,
66 telemetry_sender: TelemetrySender,
67 past_roams: Arc<Mutex<PastRoamList>>,
68) -> Result<(), anyhow::Error> {
69 let mut roam_search_result_futs: FuturesUnordered<
71 LocalBoxFuture<'static, Result<PolicyRoamRequest, Error>>,
72 > = FuturesUnordered::new();
73
74 loop {
75 select! {
76 trigger_data = trigger_data_receiver.next() => if let Some(data) = trigger_data {
78 match roam_monitor.handle_roam_trigger_data(data).await {
79 Ok(RoamTriggerDataOutcome::RoamSearch { scan_type, network_identifier, credential, current_security, reasons}) => {
80 telemetry_sender.send(TelemetryEvent::PolicyRoamScan { reasons: reasons.clone() });
81 info!("Performing scan to find proactive local roaming candidates.");
82 let roam_search_fut = get_roaming_connection_selection_future(
83 connection_selection_requester.clone(),
84 scan_type,
85 network_identifier,
86 credential,
87 current_security,
88 reasons
89 );
90 roam_search_result_futs.push(roam_search_fut.boxed());
91 },
92 Ok(RoamTriggerDataOutcome::Noop) => {},
93 Err(e) => error!("error handling roam trigger data: {}", e),
94 }
95 },
96 roam_search_result = roam_search_result_futs.select_next_some() => match roam_search_result {
99 Ok(request) => {
100 if roam_monitor.should_send_roam_request(request.clone()).unwrap_or_else(|e| {
101 error!("Error validating selected roam candidate: {}", e);
102 false
103 }) {
104 match roaming_policy {
105 RoamingPolicy::Enabled { mode: RoamingMode::CanRoam, ..} => {
106 info!("Requesting roam to candidate: {:?}", request.candidate.to_string_without_pii());
107 if roam_request_sender.try_send(request).is_err() {
108 warn!("Failed to send roam request, exiting monitor service loop.");
109 break
110 }
111 }
112 _ => {
113 debug!("Roaming policy is {:?}. Skipping roam request.", roaming_policy);
114 telemetry_sender.send(TelemetryEvent::WouldRoamConnect)
115 }
116 }
117 if let Some(mut past_roams) = past_roams.try_lock() {
119 past_roams.add(RoamEvent::new_roam_now());
120 } else {
121 error!("Unexpectedly failed to acquire lock on past roam list; will not record roam");
122 }
123 }
124 }
125 Err(e) => {
126 error!("Error occured during roam search: {:?}", e);
127 }
128 },
129 complete => {
130 debug!("Roam monitor channels dropped, exiting monitor service loop.");
131 break
132 }
133 }
134 }
135 Ok(())
136}
137
138async fn get_roaming_connection_selection_future(
141 mut connection_selection_requester: ConnectionSelectionRequester,
142 scan_type: fidl_common::ScanType,
143 network_identifier: types::NetworkIdentifier,
144 credential: Credential,
145 current_security: types::SecurityTypeDetailed,
146 reasons: Vec<RoamReason>,
147) -> Result<PolicyRoamRequest, Error> {
148 match connection_selection_requester
149 .do_roam_selection(scan_type, network_identifier, credential, current_security)
150 .await?
151 {
152 Some(candidate) => Ok(PolicyRoamRequest { candidate, reasons }),
153 None => Err(format_err!("No roam candidates found.")),
154 }
155}
156
157#[cfg(test)]
158mod test {
159 use super::*;
160 use crate::client::connection_selection::ConnectionSelectionRequest;
161 use crate::client::roaming::lib::{NUM_PLATFORM_MAX_ROAMS_PER_DAY, RoamingProfile};
162 use crate::telemetry::TelemetryEvent;
163 use crate::util::testing::fakes::FakeRoamMonitor;
164 use crate::util::testing::{
165 generate_random_network_identifier, generate_random_password,
166 generate_random_scanned_candidate,
167 };
168 use assert_matches::assert_matches;
169 use fuchsia_async::{self as fasync, TestExecutor};
170 use futures::task::Poll;
171 use futures::{Future, pin_mut};
172 use std::pin::Pin;
173 use test_case::test_case;
174 use {fidl_fuchsia_wlan_common as fidl_common, fidl_fuchsia_wlan_internal as fidl_internal};
175
176 struct TestValues {
177 trigger_data_sender: mpsc::Sender<RoamTriggerData>,
178 trigger_data_receiver: mpsc::Receiver<RoamTriggerData>,
179 roam_request_sender: mpsc::Sender<PolicyRoamRequest>,
180 roam_request_receiver: mpsc::Receiver<PolicyRoamRequest>,
181 connection_selection_requester: ConnectionSelectionRequester,
182 connection_selection_request_receiver: mpsc::Receiver<ConnectionSelectionRequest>,
183 telemetry_sender: TelemetrySender,
184 telemetry_receiver: mpsc::Receiver<TelemetryEvent>,
185 past_roams: Arc<Mutex<PastRoamList>>,
186 }
187
188 fn setup_test() -> TestValues {
189 let (trigger_data_sender, trigger_data_receiver) = mpsc::channel(100);
190 let (roam_sender, roam_receiver) = mpsc::channel(100);
191 let (connection_selection_request_sender, connection_selection_request_receiver) =
192 mpsc::channel(5);
193 let connection_selection_requester =
194 ConnectionSelectionRequester::new(connection_selection_request_sender);
195 let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100);
196 let telemetry_sender = TelemetrySender::new(telemetry_sender);
197 let past_roams = Arc::new(Mutex::new(PastRoamList::new(NUM_PLATFORM_MAX_ROAMS_PER_DAY)));
198 TestValues {
199 trigger_data_sender,
200 trigger_data_receiver,
201 roam_request_sender: roam_sender,
202 roam_request_receiver: roam_receiver,
203 connection_selection_requester,
204 connection_selection_request_receiver,
205 telemetry_sender,
206 telemetry_receiver,
207 past_roams,
208 }
209 }
210
211 #[fuchsia::test]
212 fn test_roam_data_sender_send_signal_report_ind() {
213 let _exec = TestExecutor::new();
214 let (sender, mut receiver) = mpsc::channel(100);
215 let mut roam_data_sender = RoamDataSender::new(sender);
216 let ind = fidl_internal::SignalReportIndication { rssi_dbm: -60, snr_db: 30 };
217
218 roam_data_sender.send_signal_report_ind(ind).expect("error sending signal report");
219
220 assert_matches!(receiver.try_next(), Ok(Some(RoamTriggerData::SignalReportInd(data))) => {
222 assert_eq!(ind, data);
223 });
224 }
225
226 #[test_case(RoamTriggerDataOutcome::Noop; "should not queue roam search")]
227 #[test_case(RoamTriggerDataOutcome::RoamSearch { scan_type: fidl_common::ScanType::Passive, network_identifier: generate_random_network_identifier(), credential: generate_random_password(), current_security: types::SecurityTypeDetailed::Open, reasons: vec![]}; "should queue roam search")]
228 #[fuchsia::test(add_test_attr = false)]
229 fn test_serve_loop_handles_trigger_data(response_to_should_roam_scan: RoamTriggerDataOutcome) {
230 let mut exec = TestExecutor::new();
231 let mut test_values = setup_test();
232
233 let mut roam_monitor = FakeRoamMonitor::new();
236 roam_monitor.response_to_should_roam_scan = response_to_should_roam_scan.clone();
237
238 let serve_fut = serve_roam_monitor(
240 Box::new(roam_monitor),
241 RoamingPolicy::Enabled {
242 profile: RoamingProfile::Stationary,
243 mode: RoamingMode::CanRoam,
244 },
245 test_values.trigger_data_receiver,
246 test_values.connection_selection_requester,
247 test_values.roam_request_sender,
248 test_values.telemetry_sender,
249 test_values.past_roams,
250 );
251 pin_mut!(serve_fut);
252
253 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
255
256 test_values
259 .trigger_data_sender
260 .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
261 rssi_dbm: -40,
262 snr_db: 40,
263 }))
264 .expect("failed to send");
265
266 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
268
269 match response_to_should_roam_scan {
270 RoamTriggerDataOutcome::RoamSearch { .. } => {
271 assert_matches!(
273 test_values.telemetry_receiver.try_next(),
274 Ok(Some(TelemetryEvent::PolicyRoamScan { .. }))
275 );
276 assert_matches!(
278 test_values.connection_selection_request_receiver.try_next(),
279 Ok(Some(_))
280 );
281 }
282 RoamTriggerDataOutcome::Noop => {
283 assert_matches!(
285 test_values.connection_selection_request_receiver.try_next(),
286 Err(_)
287 );
288 }
289 }
290 }
291
292 #[test_case(false, RoamingMode::CanRoam; "should not send roam request can roam")]
293 #[test_case(false, RoamingMode::MetricsOnly; "should not send roam request metrics only")]
294 #[test_case(true, RoamingMode::CanRoam; "should send roam request can roam")]
295 #[test_case(true, RoamingMode::MetricsOnly; "should send roam request metrics only")]
296 #[fuchsia::test(add_test_attr = false)]
297 fn test_serve_loop_handles_roam_search_results(
298 response_to_should_send_roam_request: bool,
299 roaming_mode: RoamingMode,
300 ) {
301 let mut exec = TestExecutor::new();
302 let mut test_values = setup_test();
303
304 let mut roam_monitor = FakeRoamMonitor::new();
307 roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
308 scan_type: fidl_common::ScanType::Passive,
309 network_identifier: generate_random_network_identifier(),
310 credential: generate_random_password(),
311 current_security: types::SecurityTypeDetailed::Open,
312 reasons: vec![],
313 };
314 roam_monitor.response_to_should_send_roam_request = response_to_should_send_roam_request;
315
316 let serve_fut = serve_roam_monitor(
318 Box::new(roam_monitor),
319 RoamingPolicy::Enabled { profile: RoamingProfile::Stationary, mode: roaming_mode },
320 test_values.trigger_data_receiver,
321 test_values.connection_selection_requester,
322 test_values.roam_request_sender,
323 test_values.telemetry_sender,
324 test_values.past_roams,
325 );
326 pin_mut!(serve_fut);
327
328 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
330
331 test_values
334 .trigger_data_sender
335 .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
336 rssi_dbm: -40,
337 snr_db: 40,
338 }))
339 .expect("failed to send");
340
341 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
343
344 let candidate = generate_random_scanned_candidate();
346 assert_matches!(test_values.connection_selection_request_receiver.try_next(), Ok(Some(ConnectionSelectionRequest::RoamSelection { responder, .. })) => {
347 responder.send(Some(candidate.clone())).expect("failed to send");
349 });
350
351 assert_matches!(exec.run_until_stalled(&mut serve_fut), Poll::Pending);
353
354 assert_matches!(
356 test_values.telemetry_receiver.try_next(),
357 Ok(Some(TelemetryEvent::PolicyRoamScan { .. }))
358 );
359
360 if response_to_should_send_roam_request && roaming_mode == RoamingMode::CanRoam {
361 assert_matches!(test_values.roam_request_receiver.try_next(), Ok(Some(selection)) => {
364 assert_eq!(selection.candidate, candidate);
365 });
366 } else {
367 assert_matches!(test_values.roam_request_receiver.try_next(), Err(_));
370 }
371 }
372
373 #[fuchsia::test]
374 fn test_roam_attempts_are_recorded_in_past_roams() {
375 let mut exec = TestExecutor::new();
376 let mut test_values = setup_test();
377
378 let mut roam_monitor = FakeRoamMonitor::new();
381 roam_monitor.response_to_should_roam_scan = RoamTriggerDataOutcome::RoamSearch {
382 scan_type: fidl_common::ScanType::Passive,
383 network_identifier: generate_random_network_identifier(),
384 credential: generate_random_password(),
385 current_security: types::SecurityTypeDetailed::Open,
386 reasons: vec![],
387 };
388 roam_monitor.response_to_should_send_roam_request = true;
389
390 let serve_fut = serve_roam_monitor(
392 Box::new(roam_monitor),
393 RoamingPolicy::Enabled {
394 profile: RoamingProfile::Stationary,
395 mode: RoamingMode::CanRoam,
396 },
397 test_values.trigger_data_receiver,
398 test_values.connection_selection_requester,
399 test_values.roam_request_sender,
400 test_values.telemetry_sender,
401 test_values.past_roams.clone(),
402 );
403 pin_mut!(serve_fut);
404
405 trigger_scan_and_roam(
406 &mut exec,
407 &mut serve_fut,
408 &mut test_values.trigger_data_sender,
409 &mut test_values.connection_selection_request_receiver,
410 &mut test_values.roam_request_receiver,
411 );
412
413 let past_roams = test_values
415 .past_roams
416 .clone()
417 .try_lock()
418 .unwrap()
419 .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
420 assert_eq!(past_roams.len(), 1);
421
422 trigger_scan_and_roam(
423 &mut exec,
424 &mut serve_fut,
425 &mut test_values.trigger_data_sender,
426 &mut test_values.connection_selection_request_receiver,
427 &mut test_values.roam_request_receiver,
428 );
429
430 let past_roams = test_values
432 .past_roams
433 .clone()
434 .try_lock()
435 .unwrap()
436 .get_recent(fasync::MonotonicInstant::INFINITE_PAST);
437 assert_eq!(past_roams.len(), 2);
438 }
439
440 fn trigger_scan_and_roam(
444 exec: &mut TestExecutor,
445 serve_fut: &mut Pin<&mut impl Future<Output = std::result::Result<(), anyhow::Error>>>,
446 trigger_data_sender: &mut mpsc::Sender<RoamTriggerData>,
447 connection_selection_request_receiver: &mut mpsc::Receiver<ConnectionSelectionRequest>,
448 roam_request_receiver: &mut mpsc::Receiver<PolicyRoamRequest>,
449 ) {
450 assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
452
453 trigger_data_sender
456 .try_send(RoamTriggerData::SignalReportInd(fidl_internal::SignalReportIndication {
457 rssi_dbm: -40,
458 snr_db: 40,
459 }))
460 .expect("failed to send");
461
462 assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
464
465 let candidate = generate_random_scanned_candidate();
467 assert_matches!(connection_selection_request_receiver.try_next(), Ok(Some(ConnectionSelectionRequest::RoamSelection { responder, .. })) => {
468 responder.send(Some(candidate.clone())).expect("failed to send");
470 });
471
472 assert_matches!(exec.run_until_stalled(serve_fut), Poll::Pending);
473 assert_matches!(roam_request_receiver.try_next(), Ok(Some(selection)) => {
474 assert_eq!(selection.candidate, candidate);
475 });
476 }
477}