| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::{ |
| client::{roaming::local_roam_manager::LocalRoamManagerApi, types}, |
| config_management::{PastConnectionData, SavedNetworksManagerApi}, |
| mode_management::{Defect, IfaceFailure}, |
| telemetry::{ |
| DisconnectInfo, TelemetryEvent, TelemetrySender, AVERAGE_SCORE_DELTA_MINIMUM_DURATION, |
| METRICS_SHORT_CONNECT_DURATION, |
| }, |
| util::{ |
| historical_list::HistoricalList, |
| listener::{ |
| ClientListenerMessageSender, ClientNetworkState, ClientStateUpdate, |
| Message::NotifyListeners, |
| }, |
| state_machine::{self, ExitReason, IntoStateExt}, |
| }, |
| }, |
| anyhow::format_err, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_internal as fidl_internal, |
| fidl_fuchsia_wlan_policy as fidl_policy, fidl_fuchsia_wlan_sme as fidl_sme, |
| fuchsia_async::{self as fasync, DurationExt, TimeoutExt}, |
| fuchsia_zircon as zx, |
| futures::{ |
| channel::{mpsc, oneshot}, |
| future::FutureExt, |
| lock::Mutex, |
| select, |
| stream::{self, StreamExt, TryStreamExt}, |
| }, |
| std::{convert::Infallible, sync::Arc}, |
| tracing::{debug, error, info, warn}, |
| wlan_common::{bss::BssDescription, sequestered::Sequestered}, |
| }; |
| |
| const MAX_CONNECTION_ATTEMPTS: u8 = 4; // arbitrarily chosen until we have some data |
| const CONNECT_TIMEOUT: zx::Duration = zx::Duration::from_seconds(30); |
| const NUM_PAST_SCORES: usize = 91; // number of past periodic connection scores to store for metrics |
| |
| type State = state_machine::State<ExitReason>; |
| type ReqStream = stream::Fuse<mpsc::Receiver<ManualRequest>>; |
| |
| pub trait ClientApi { |
| fn connect(&mut self, selection: types::ConnectSelection) -> Result<(), anyhow::Error>; |
| fn disconnect( |
| &mut self, |
| reason: types::DisconnectReason, |
| responder: oneshot::Sender<()>, |
| ) -> Result<(), anyhow::Error>; |
| |
| /// Queries the liveness of the channel used to control the client state machine. If the |
| /// channel is not alive, this indicates that the client state machine has exited. |
| fn is_alive(&self) -> bool; |
| } |
| |
| pub struct Client { |
| req_sender: mpsc::Sender<ManualRequest>, |
| } |
| |
| impl Client { |
| pub fn new(req_sender: mpsc::Sender<ManualRequest>) -> Self { |
| Self { req_sender } |
| } |
| } |
| |
| impl ClientApi for Client { |
| fn connect(&mut self, selection: types::ConnectSelection) -> Result<(), anyhow::Error> { |
| self.req_sender |
| .try_send(ManualRequest::Connect(selection)) |
| .map_err(|e| format_err!("failed to send connect selection: {:?}", e)) |
| } |
| |
| fn disconnect( |
| &mut self, |
| reason: types::DisconnectReason, |
| responder: oneshot::Sender<()>, |
| ) -> Result<(), anyhow::Error> { |
| self.req_sender |
| .try_send(ManualRequest::Disconnect((reason, responder))) |
| .map_err(|e| format_err!("failed to send disconnect request: {:?}", e)) |
| } |
| |
| fn is_alive(&self) -> bool { |
| !self.req_sender.is_closed() |
| } |
| } |
| |
| // TODO(https://fxbug.dev/324167674): fix. |
| #[allow(clippy::large_enum_variant)] |
| pub enum ManualRequest { |
| Connect(types::ConnectSelection), |
| Disconnect((types::DisconnectReason, oneshot::Sender<()>)), |
| } |
| |
| fn send_listener_state_update( |
| sender: &ClientListenerMessageSender, |
| network_update: Option<ClientNetworkState>, |
| ) { |
| let mut networks = vec![]; |
| if let Some(network) = network_update { |
| networks.push(network) |
| } |
| |
| let updates = |
| ClientStateUpdate { state: fidl_policy::WlanClientState::ConnectionsEnabled, networks }; |
| match sender.clone().unbounded_send(NotifyListeners(updates)) { |
| Ok(_) => (), |
| Err(e) => error!("failed to send state update: {:?}", e), |
| }; |
| } |
| |
| pub async fn serve( |
| iface_id: u16, |
| proxy: fidl_sme::ClientSmeProxy, |
| sme_event_stream: fidl_sme::ClientSmeEventStream, |
| req_stream: mpsc::Receiver<ManualRequest>, |
| update_sender: ClientListenerMessageSender, |
| saved_networks_manager: Arc<dyn SavedNetworksManagerApi>, |
| connect_selection: Option<types::ConnectSelection>, |
| telemetry_sender: TelemetrySender, |
| defect_sender: mpsc::UnboundedSender<Defect>, |
| roam_manager: Arc<Mutex<dyn LocalRoamManagerApi>>, |
| ) { |
| let next_network = connect_selection |
| .map(|selection| ConnectingOptions { connect_selection: selection, attempt_counter: 0 }); |
| let disconnect_options = DisconnectingOptions { |
| disconnect_responder: None, |
| previous_network: None, |
| next_network, |
| reason: types::DisconnectReason::Startup, |
| }; |
| let common_options = CommonStateOptions { |
| proxy, |
| req_stream: req_stream.fuse(), |
| update_sender, |
| saved_networks_manager, |
| telemetry_sender, |
| iface_id, |
| defect_sender, |
| roam_manager, |
| }; |
| let state_machine = |
| disconnecting_state(common_options, disconnect_options).into_state_machine(); |
| let removal_watcher = sme_event_stream.map_ok(|_| ()).try_collect::<()>(); |
| select! { |
| state_machine = state_machine.fuse() => { |
| match state_machine { |
| Ok(v) => { |
| // This should never happen because the `Infallible` type should be impossible |
| // to create. |
| let _: Infallible = v; |
| unreachable!() |
| } |
| Err(ExitReason(Err(e))) => error!("Client state machine for iface #{} terminated with an error: {:?}", |
| iface_id, e), |
| Err(ExitReason(Ok(_))) => info!("Client state machine for iface #{} exited gracefully", |
| iface_id,), |
| } |
| } |
| removal_watcher = removal_watcher.fuse() => if let Err(e) = removal_watcher { |
| info!("Error reading from Client SME channel of iface #{}: {:?}", |
| iface_id, e); |
| }, |
| } |
| } |
| |
| /// Common parameters passed to all states |
| struct CommonStateOptions { |
| proxy: fidl_sme::ClientSmeProxy, |
| req_stream: ReqStream, |
| update_sender: ClientListenerMessageSender, |
| saved_networks_manager: Arc<dyn SavedNetworksManagerApi>, |
| telemetry_sender: TelemetrySender, |
| iface_id: u16, |
| defect_sender: mpsc::UnboundedSender<Defect>, |
| roam_manager: Arc<Mutex<dyn LocalRoamManagerApi>>, |
| } |
| |
| pub type ConnectionStatsSender = mpsc::UnboundedSender<fidl_internal::SignalReportIndication>; |
| pub type ConnectionStatsReceiver = mpsc::UnboundedReceiver<fidl_internal::SignalReportIndication>; |
| |
| fn handle_none_request() -> Result<State, ExitReason> { |
| return Err(ExitReason(Err(format_err!("The stream of requests ended unexpectedly")))); |
| } |
| |
| // These functions were introduced to resolve the following error: |
| // ``` |
| // error[E0391]: cycle detected when evaluating trait selection obligation |
| // `impl core::future::future::Future: std::marker::Send` |
| // ``` |
| // which occurs when two functions that return an `impl Trait` call each other |
| // in a cycle. (e.g. this case `connecting_state` calling `disconnecting_state`, |
| // which calls `connecting_state`) |
| fn to_disconnecting_state( |
| common_options: CommonStateOptions, |
| disconnecting_options: DisconnectingOptions, |
| ) -> State { |
| disconnecting_state(common_options, disconnecting_options).into_state() |
| } |
| fn to_connecting_state( |
| common_options: CommonStateOptions, |
| connecting_options: ConnectingOptions, |
| ) -> State { |
| connecting_state(common_options, connecting_options).into_state() |
| } |
| |
| struct DisconnectingOptions { |
| disconnect_responder: Option<oneshot::Sender<()>>, |
| /// Information about the previously connected network, if there was one. Used to send out |
| /// listener updates. |
| previous_network: Option<(types::NetworkIdentifier, types::DisconnectStatus)>, |
| /// Configuration for the next network to connect to, after the disconnect is complete. If not |
| /// present, the state machine will proceed to IDLE. |
| next_network: Option<ConnectingOptions>, |
| reason: types::DisconnectReason, |
| } |
| /// The DISCONNECTING state requests an SME disconnect, then transitions to either: |
| /// - the CONNECTING state if options.next_network is present |
| /// - exit otherwise |
| async fn disconnecting_state( |
| common_options: CommonStateOptions, |
| options: DisconnectingOptions, |
| ) -> Result<State, ExitReason> { |
| // Log a message with the disconnect reason |
| match options.reason { |
| types::DisconnectReason::FailedToConnect |
| | types::DisconnectReason::Startup |
| | types::DisconnectReason::DisconnectDetectedFromSme => { |
| // These are either just noise or have separate logging, so keep the level at debug. |
| debug!("Disconnected due to {:?}", options.reason); |
| } |
| reason => { |
| info!("Disconnected due to {:?}", reason); |
| } |
| } |
| |
| // TODO(https://fxbug.dev/42130926): either make this fire-and-forget in the SME, or spawn a thread for this, |
| // so we don't block on it |
| common_options |
| .proxy |
| .disconnect(types::convert_to_sme_disconnect_reason(options.reason)) |
| .await |
| .map_err(|e| { |
| ExitReason(Err(format_err!("Failed to send command to wlanstack: {:?}", e))) |
| })?; |
| |
| // Notify listeners if a disconnect request was sent, or ensure that listeners know client |
| // connections are enabled. |
| let networks = |
| options.previous_network.map(|(network_identifier, status)| ClientNetworkState { |
| id: network_identifier, |
| state: types::ConnectionState::Disconnected, |
| status: Some(status), |
| }); |
| send_listener_state_update(&common_options.update_sender, networks); |
| |
| // Notify the caller that disconnect was sent to the SME once the final disconnected update has |
| // been sent. This ensures that there will not be a race when the IfaceManager sends out a |
| // ConnectionsDisabled update. |
| match options.disconnect_responder { |
| Some(responder) => responder.send(()).unwrap_or_else(|_| ()), |
| None => (), |
| } |
| |
| // Transition to next state |
| match options.next_network { |
| Some(next_network) => Ok(to_connecting_state(common_options, next_network)), |
| None => Err(ExitReason(Ok(()))), |
| } |
| } |
| |
| fn connect_txn_event_name(event: &fidl_sme::ConnectTransactionEvent) -> &'static str { |
| match event { |
| fidl_sme::ConnectTransactionEvent::OnConnectResult { .. } => "OnConnectResult", |
| fidl_sme::ConnectTransactionEvent::OnDisconnect { .. } => "OnDisconnect", |
| fidl_sme::ConnectTransactionEvent::OnSignalReport { .. } => "OnSignalReport", |
| fidl_sme::ConnectTransactionEvent::OnChannelSwitched { .. } => "OnChannelSwitched", |
| } |
| } |
| |
| struct ConnectingOptions { |
| connect_selection: types::ConnectSelection, |
| /// Count of previous consecutive failed connection attempts to this same network. |
| attempt_counter: u8, |
| } |
| |
| async fn handle_connecting_error_and_retry( |
| common_options: CommonStateOptions, |
| options: ConnectingOptions, |
| ) -> Result<State, ExitReason> { |
| // Check if the limit for connection attempts to this network has been |
| // exceeded. |
| let new_attempt_count = options.attempt_counter + 1; |
| if new_attempt_count >= MAX_CONNECTION_ATTEMPTS { |
| info!("Exceeded maximum connection attempts, will not retry"); |
| send_listener_state_update( |
| &common_options.update_sender, |
| Some(ClientNetworkState { |
| id: options.connect_selection.target.network, |
| state: types::ConnectionState::Failed, |
| status: Some(types::DisconnectStatus::ConnectionFailed), |
| }), |
| ); |
| return Err(ExitReason(Ok(()))); |
| } else { |
| // Limit not exceeded, retry after backing off. |
| let backoff_time = 400_i64 * i64::from(new_attempt_count); |
| info!("Will attempt to reconnect after {}ms backoff", backoff_time); |
| fasync::Timer::new(zx::Duration::from_millis(backoff_time).after_now()).await; |
| |
| let next_connecting_options = ConnectingOptions { |
| connect_selection: types::ConnectSelection { |
| reason: types::ConnectReason::RetryAfterFailedConnectAttempt, |
| ..options.connect_selection |
| }, |
| attempt_counter: new_attempt_count, |
| }; |
| let disconnecting_options = DisconnectingOptions { |
| disconnect_responder: None, |
| previous_network: None, |
| next_network: Some(next_connecting_options), |
| reason: types::DisconnectReason::FailedToConnect, |
| }; |
| return Ok(to_disconnecting_state(common_options, disconnecting_options)); |
| } |
| } |
| |
| /// Wait until stream returns an OnConnectResult event or None. Ignore other event types. |
| async fn wait_for_connect_result( |
| mut stream: fidl_sme::ConnectTransactionEventStream, |
| ) -> Result<fidl_sme::ConnectResult, ExitReason> { |
| loop { |
| let stream_fut = stream.try_next(); |
| match stream_fut.await.map_err(|e| { |
| ExitReason(Err(format_err!("Failed to receive connect result from sme: {:?}", e))) |
| })? { |
| Some(fidl_sme::ConnectTransactionEvent::OnConnectResult { result }) => { |
| return Ok(result) |
| } |
| Some(other) => { |
| info!( |
| "Expected ConnectTransactionEvent::OnConnectResult, got {}. Ignoring.", |
| connect_txn_event_name(&other) |
| ); |
| } |
| None => { |
| return Err(ExitReason(Err(format_err!( |
| "Server closed the ConnectTransaction channel before sending a response" |
| )))); |
| } |
| }; |
| } |
| } |
| |
| /// The CONNECTING state requests an SME connect. It handles the SME connect response: |
| /// - for a successful connection, transition to CONNECTED state |
| /// - for a failed connection, retry connection by passing a next_network to the |
| /// DISCONNECTING state, as long as there haven't been too many connection attempts |
| /// During this time, incoming ManualRequests are also monitored for: |
| /// - duplicate connect requests are deduped |
| /// - different connect requests are serviced by passing a next_network to the DISCONNECTING state |
| /// - disconnect requests cause a transition to DISCONNECTING state |
| async fn connecting_state<'a>( |
| common_options: CommonStateOptions, |
| options: ConnectingOptions, |
| ) -> Result<State, ExitReason> { |
| debug!("Entering connecting state"); |
| |
| if options.attempt_counter > 0 { |
| info!( |
| "Retrying connection, {} attempts remaining", |
| MAX_CONNECTION_ATTEMPTS - options.attempt_counter |
| ); |
| } |
| |
| // Send a "Connecting" update to listeners, unless this is a retry |
| if options.attempt_counter == 0 { |
| send_listener_state_update( |
| &common_options.update_sender, |
| Some(ClientNetworkState { |
| id: options.connect_selection.target.network.clone(), |
| state: types::ConnectionState::Connecting, |
| status: None, |
| }), |
| ); |
| }; |
| |
| // Release the sequestered BSS description. While considered a "black box" elsewhere, the state |
| // machine uses this by design to construct its AP state and to report telemetry. |
| let bss_description = |
| Sequestered::release(options.connect_selection.target.bss.bss_description.clone()); |
| let ap_state = types::ApState::from( |
| BssDescription::try_from(bss_description.clone()).map_err(|error| { |
| // This only occurs if an invalid `BssDescription` is received from SME, which should |
| // never happen. |
| ExitReason(Err( |
| format_err!("Failed to convert BSS description from FIDL: {:?}", error,), |
| )) |
| })?, |
| ); |
| |
| // Send a connect request to the SME. |
| let (connect_txn, remote) = create_proxy() |
| .map_err(|e| ExitReason(Err(format_err!("Failed to create proxy: {:?}", e))))?; |
| let sme_connect_request = fidl_sme::ConnectRequest { |
| ssid: options.connect_selection.target.network.ssid.to_vec(), |
| bss_description, |
| multiple_bss_candidates: options.connect_selection.target.network_has_multiple_bss, |
| authentication: options.connect_selection.target.authenticator.clone().into(), |
| deprecated_scan_type: fidl_fuchsia_wlan_common::ScanType::Active, |
| }; |
| common_options.proxy.connect(&sme_connect_request, Some(remote)).map_err(|e| { |
| ExitReason(Err(format_err!("Failed to send command to wlanstack: {:?}", e))) |
| })?; |
| let start_time = fasync::Time::now(); |
| |
| // Wait for connect result or timeout. |
| let stream = connect_txn.take_event_stream(); |
| let sme_result = wait_for_connect_result(stream) |
| .on_timeout(CONNECT_TIMEOUT, || { |
| Err(ExitReason(Err(format_err!("Timed out waiting for connect result from SME.")))) |
| }) |
| .await?; |
| |
| // Report the connect result to the saved networks manager. |
| common_options |
| .saved_networks_manager |
| .record_connect_result( |
| options.connect_selection.target.network.clone(), |
| &options.connect_selection.target.credential, |
| ap_state.original().bssid, |
| sme_result, |
| options.connect_selection.target.bss.observation, |
| ) |
| .await; |
| |
| let network_is_likely_hidden = match common_options |
| .saved_networks_manager |
| .lookup(&options.connect_selection.target.network) |
| .await |
| .iter() |
| .find(|&config| config.credential == options.connect_selection.target.credential) |
| { |
| Some(config) => config.is_hidden(), |
| None => { |
| error!("Could not lookup if connected network is hidden."); |
| false |
| } |
| }; |
| |
| // Log the connect result for metrics. |
| common_options.telemetry_sender.send(TelemetryEvent::ConnectResult { |
| ap_state: ap_state.clone(), |
| result: sme_result, |
| policy_connect_reason: Some(options.connect_selection.reason), |
| multiple_bss_candidates: options.connect_selection.target.network_has_multiple_bss, |
| iface_id: common_options.iface_id, |
| network_is_likely_hidden, |
| }); |
| |
| match (sme_result.code, sme_result.is_credential_rejected) { |
| (fidl_ieee80211::StatusCode::Success, _) => { |
| info!("Successfully connected to network"); |
| send_listener_state_update( |
| &common_options.update_sender, |
| Some(ClientNetworkState { |
| id: options.connect_selection.target.network.clone(), |
| state: types::ConnectionState::Connected, |
| status: None, |
| }), |
| ); |
| let connected_options = ConnectedOptions { |
| currently_fulfilled_connection: options.connect_selection.clone(), |
| connect_txn_stream: connect_txn.take_event_stream(), |
| ap_state: Box::new(ap_state), |
| multiple_bss_candidates: options.connect_selection.target.network_has_multiple_bss, |
| connection_attempt_time: start_time, |
| time_to_connect: fasync::Time::now() - start_time, |
| network_is_likely_hidden, |
| }; |
| return Ok(connected_state(common_options, connected_options).into_state()); |
| } |
| (code, true) => { |
| info!("Failed to connect: {:?}. Will not retry because of credential error.", code); |
| send_listener_state_update( |
| &common_options.update_sender, |
| Some(ClientNetworkState { |
| id: options.connect_selection.target.network, |
| state: types::ConnectionState::Failed, |
| status: Some(types::DisconnectStatus::CredentialsFailed), |
| }), |
| ); |
| return Err(ExitReason(Ok(()))); |
| } |
| (code, _) => { |
| info!("Failed to connect: {:?}", code); |
| |
| // Defects should be logged for connection failures that are not due to |
| // bad credentials. |
| if let Err(e) = common_options.defect_sender.unbounded_send(Defect::Iface( |
| IfaceFailure::ConnectionFailure { iface_id: common_options.iface_id }, |
| )) { |
| warn!("Failed to log connection failure: {}", e); |
| } |
| |
| return handle_connecting_error_and_retry(common_options, options).await; |
| } |
| }; |
| } |
| |
| struct ConnectedOptions { |
| // Keep track of the BSSID we are connected in order to record connection information for |
| // future network selection. |
| ap_state: Box<types::ApState>, |
| multiple_bss_candidates: bool, |
| currently_fulfilled_connection: types::ConnectSelection, |
| connect_txn_stream: fidl_sme::ConnectTransactionEventStream, |
| network_is_likely_hidden: bool, |
| /// Time at which connect was first attempted, historical data for network scoring. |
| pub connection_attempt_time: fasync::Time, |
| /// Duration from connection attempt to success, historical data for network scoring. |
| pub time_to_connect: zx::Duration, |
| } |
| |
| /// The CONNECTED state monitors the SME status. It handles the SME status response: |
| /// - if still connected to the correct network, no action |
| /// - if disconnected, retry connection by passing a next_network to the |
| /// DISCONNECTING state |
| /// During this time, incoming ManualRequests are also monitored for: |
| /// - duplicate connect requests are deduped |
| /// - different connect requests are serviced by passing a next_network to the DISCONNECTING state |
| /// - disconnect requests cause a transition to DISCONNECTING state |
| async fn connected_state( |
| mut common_options: CommonStateOptions, |
| mut options: ConnectedOptions, |
| ) -> Result<State, ExitReason> { |
| debug!("Entering connected state"); |
| let mut connect_start_time = fasync::Time::now(); |
| |
| // Tracked signals |
| let mut past_signals = HistoricalList::new(NUM_PAST_SCORES); |
| past_signals.add(types::TimestampedSignal { |
| time: fasync::Time::now(), |
| signal: options.ap_state.tracked.signal, |
| }); |
| |
| let initial_signal = options.ap_state.tracked.signal; |
| |
| // Used to receive roam requests. The sender is cloned to send to the RoamManager. |
| let (roam_sender, mut roam_receiver) = mpsc::unbounded::<types::ScannedCandidate>(); |
| let mut roam_monitor = common_options.roam_manager.lock().await.get_roam_monitor( |
| options.ap_state.tracked.signal, |
| options.currently_fulfilled_connection.clone(), |
| roam_sender, |
| ); |
| |
| // Timer to log post-connection scores metrics. |
| let mut post_connect_metric_timer = |
| fasync::Timer::new(AVERAGE_SCORE_DELTA_MINIMUM_DURATION.after_now()).fuse(); |
| |
| // Timer when duration has lapsed from short duration to long duration, for metrics purposes. |
| let mut connect_duration_metric_timer = |
| fasync::Timer::new(METRICS_SHORT_CONNECT_DURATION.after_now()).fuse(); |
| |
| loop { |
| select! { |
| event = options.connect_txn_stream.next() => match event { |
| Some(Ok(event)) => { |
| let is_sme_idle = match event { |
| fidl_sme::ConnectTransactionEvent::OnDisconnect { info: fidl_info } => { |
| // Log a disconnect in Cobalt |
| let now = fasync::Time::now(); |
| let info = DisconnectInfo { |
| connected_duration: now - connect_start_time, |
| is_sme_reconnecting: fidl_info.is_sme_reconnecting, |
| disconnect_source: fidl_info.disconnect_source, |
| previous_connect_reason: options.currently_fulfilled_connection.reason, |
| ap_state: (*options.ap_state).clone(), |
| signals: past_signals.clone(), |
| }; |
| common_options.telemetry_sender.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info }); |
| |
| // Record data about the connection and disconnect for future network |
| // selection. |
| record_disconnect( |
| &common_options, |
| &options, |
| connect_start_time, |
| types::DisconnectReason::DisconnectDetectedFromSme, |
| options.ap_state.tracked.signal, |
| ).await; |
| |
| !fidl_info.is_sme_reconnecting |
| } |
| fidl_sme::ConnectTransactionEvent::OnConnectResult { result } => { |
| let connected = result.code == fidl_ieee80211::StatusCode::Success; |
| if connected { |
| // This OnConnectResult should be for reconnecting to the same AP, |
| // so keep the same SignalData but reset the connect start time |
| // to track as a new connection. |
| connect_start_time = fasync::Time::now(); |
| } |
| common_options.telemetry_sender.send(TelemetryEvent::ConnectResult { |
| iface_id: common_options.iface_id, |
| result, |
| policy_connect_reason: None, |
| // It's not necessarily true that there are still multiple BSS |
| // candidates in the network at this point in time, but we use the |
| // heuristic that if previously there were multiple BSS's, then |
| // it likely remains the same. |
| multiple_bss_candidates: options.multiple_bss_candidates, |
| ap_state: (*options.ap_state).clone(), |
| network_is_likely_hidden: options.network_is_likely_hidden, |
| }); |
| !connected |
| } |
| fidl_sme::ConnectTransactionEvent::OnSignalReport { ind } => { |
| // Update connection data |
| options.ap_state.tracked.signal = ind.into(); |
| |
| // Update list of signals |
| past_signals.add(types::TimestampedSignal { |
| time: fasync::Time::now(), |
| signal: ind.into(), |
| }); |
| |
| // Send signal report metrics |
| common_options.telemetry_sender.send(TelemetryEvent::OnSignalReport { |
| ind |
| }); |
| |
| // Send indication to roam_monitor |
| let _ = roam_monitor.handle_connection_stats(ind); |
| |
| false |
| } |
| fidl_sme::ConnectTransactionEvent::OnChannelSwitched { info } => { |
| options.ap_state.tracked.channel.primary = info.new_channel; |
| common_options.telemetry_sender.send(TelemetryEvent::OnChannelSwitched { info }); |
| false |
| } |
| }; |
| |
| if is_sme_idle { |
| info!("Idle sme detected."); |
| let options = DisconnectingOptions { |
| disconnect_responder: None, |
| previous_network: Some((options.currently_fulfilled_connection.target.network.clone(), types::DisconnectStatus::ConnectionFailed)), |
| next_network: None, |
| reason: types::DisconnectReason::DisconnectDetectedFromSme, |
| }; |
| return Ok(disconnecting_state(common_options, options).into_state()); |
| } |
| } |
| _ => { |
| info!("SME dropped ConnectTransaction channel. Exiting state machine"); |
| return Err(ExitReason(Err(format_err!("Failed to receive ConnectTransactionEvent for SME status")))); |
| } |
| }, |
| req = common_options.req_stream.next() => { |
| let now = fasync::Time::now(); |
| match req { |
| Some(ManualRequest::Disconnect((reason, responder))) => { |
| debug!("Disconnect requested"); |
| record_disconnect( |
| &common_options, |
| &options, |
| connect_start_time, |
| reason, |
| options.ap_state.tracked.signal, |
| ).await; |
| let ConnectedOptions {ap_state, currently_fulfilled_connection, ..} = options; |
| let options = DisconnectingOptions { |
| disconnect_responder: Some(responder), |
| previous_network: Some((currently_fulfilled_connection.target.network.clone(), types::DisconnectStatus::ConnectionStopped)), |
| next_network: None, |
| reason, |
| }; |
| let info = DisconnectInfo { |
| connected_duration: now - connect_start_time, |
| is_sme_reconnecting: false, |
| disconnect_source: fidl_sme::DisconnectSource::User(types::convert_to_sme_disconnect_reason(options.reason)), |
| ap_state: *ap_state, |
| previous_connect_reason: currently_fulfilled_connection.reason, |
| signals: past_signals.clone(), |
| }; |
| common_options.telemetry_sender.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info }); |
| return Ok(disconnecting_state(common_options, options).into_state()); |
| } |
| Some(ManualRequest::Connect(new_connect_selection)) => { |
| // Check if it's the same network as we're currently connected to. If yes, reply immediately |
| if new_connect_selection.target.network == options.currently_fulfilled_connection.target.network { |
| info!("Received connection request for current network, deduping"); |
| } else { |
| let disconnect_reason = convert_manual_connect_to_disconnect_reason(&new_connect_selection.reason).unwrap_or_else(|_| { |
| error!("Unexpected connection reason: {:?}", new_connect_selection.reason); |
| types::DisconnectReason::Unknown |
| }); |
| |
| record_disconnect( |
| &common_options, |
| &options, |
| connect_start_time, |
| disconnect_reason, |
| options.ap_state.tracked.signal, |
| ).await; |
| |
| |
| let next_connecting_options = ConnectingOptions { |
| connect_selection: new_connect_selection.clone(), |
| attempt_counter: 0, |
| }; |
| let ConnectedOptions { ap_state, currently_fulfilled_connection, ..} = options; |
| let options = DisconnectingOptions { |
| disconnect_responder: None, |
| previous_network: Some((currently_fulfilled_connection.target.network, types::DisconnectStatus::ConnectionStopped)), |
| next_network: Some(next_connecting_options), |
| reason: disconnect_reason, |
| }; |
| info!("Connection to new network requested, disconnecting from current network"); |
| let info = DisconnectInfo { |
| connected_duration: now - connect_start_time, |
| is_sme_reconnecting: false, |
| disconnect_source: fidl_sme::DisconnectSource::User(types::convert_to_sme_disconnect_reason(options.reason)), |
| ap_state: *ap_state, |
| previous_connect_reason: currently_fulfilled_connection.reason, |
| signals: past_signals.clone(), |
| }; |
| common_options.telemetry_sender.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info }); |
| return Ok(disconnecting_state(common_options, options).into_state()) |
| } |
| } |
| None => return handle_none_request(), |
| }; |
| }, |
| () = post_connect_metric_timer => { |
| common_options.telemetry_sender.send( |
| TelemetryEvent::PostConnectionSignals { connect_time: connect_start_time, signal_at_connect: initial_signal, signals: past_signals.clone() } |
| ); |
| }, |
| () = connect_duration_metric_timer => { |
| // Log the average connection score metric for a long duration connection. |
| common_options.telemetry_sender.send( |
| TelemetryEvent::LongDurationSignals{ signals: past_signals.get_before(fasync::Time::now()) } |
| ); |
| } |
| _roam_request = roam_receiver.next() => { |
| // TODO(nmccracken) Roam to this network once we have decided proactive roaming is ready to enable. |
| } |
| } |
| } |
| } |
| |
| async fn record_disconnect( |
| common_options: &CommonStateOptions, |
| options: &ConnectedOptions, |
| connect_start_time: fasync::Time, |
| reason: types::DisconnectReason, |
| signal: types::Signal, |
| ) { |
| let curr_time = fasync::Time::now(); |
| let uptime = curr_time - connect_start_time; |
| let data = PastConnectionData::new( |
| options.ap_state.original().bssid, |
| options.connection_attempt_time, |
| options.time_to_connect, |
| curr_time, |
| uptime, |
| reason, |
| signal, |
| // TODO: record average phy rate over connection once available |
| 0, |
| ); |
| common_options |
| .saved_networks_manager |
| .record_disconnect( |
| &options.currently_fulfilled_connection.target.network.clone(), |
| &options.currently_fulfilled_connection.target.credential, |
| data, |
| ) |
| .await; |
| } |
| |
| /// Get the disconnect reason corresponding to the connect reason. Return an error if the connect |
| /// reason does not correspond to a manual connect. |
| pub fn convert_manual_connect_to_disconnect_reason( |
| reason: &types::ConnectReason, |
| ) -> Result<types::DisconnectReason, ()> { |
| match reason { |
| types::ConnectReason::FidlConnectRequest => Ok(types::DisconnectReason::FidlConnectRequest), |
| types::ConnectReason::ProactiveNetworkSwitch => { |
| Ok(types::DisconnectReason::ProactiveNetworkSwitch) |
| } |
| types::ConnectReason::RetryAfterDisconnectDetected |
| | types::ConnectReason::RetryAfterFailedConnectAttempt |
| | types::ConnectReason::RegulatoryChangeReconnect |
| | types::ConnectReason::IdleInterfaceAutoconnect |
| | types::ConnectReason::NewSavedNetworkAutoconnect => Err(()), |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| |
| use { |
| super::*, |
| crate::{ |
| config_management::{ |
| network_config::{self, Credential, FailureReason}, |
| PastConnectionList, SavedNetworksManager, |
| }, |
| util::{ |
| listener, |
| testing::{ |
| generate_connect_selection, generate_disconnect_info, poll_sme_req, |
| random_connection_data, ConnectResultRecord, ConnectionRecord, |
| FakeLocalRoamManager, FakeSavedNetworksManager, |
| }, |
| }, |
| }, |
| fidl::{endpoints::create_proxy_and_stream, prelude::*}, |
| fidl_fuchsia_stash as fidl_stash, fidl_fuchsia_wlan_policy as fidl_policy, |
| fuchsia_zircon::prelude::*, |
| futures::{task::Poll, Future}, |
| lazy_static::lazy_static, |
| std::pin::pin, |
| wlan_common::assert_variant, |
| wlan_metrics_registry::PolicyDisconnectionMigratedMetricDimensionReason, |
| }; |
| |
| lazy_static! { |
| pub static ref TEST_PASSWORD: Credential = Credential::Password(b"password".to_vec()); |
| pub static ref TEST_WEP_PSK: Credential = Credential::Password(b"five0".to_vec()); |
| } |
| |
| struct TestValues { |
| common_options: CommonStateOptions, |
| sme_req_stream: fidl_sme::ClientSmeRequestStream, |
| saved_networks_manager: Arc<FakeSavedNetworksManager>, |
| client_req_sender: mpsc::Sender<ManualRequest>, |
| update_receiver: mpsc::UnboundedReceiver<listener::ClientListenerMessage>, |
| telemetry_receiver: mpsc::Receiver<TelemetryEvent>, |
| defect_receiver: mpsc::UnboundedReceiver<Defect>, |
| stats_receiver: mpsc::UnboundedReceiver<fidl_internal::SignalReportIndication>, |
| } |
| |
| fn test_setup() -> TestValues { |
| let (client_req_sender, client_req_stream) = mpsc::channel(1); |
| let (update_sender, update_receiver) = mpsc::unbounded(); |
| let (sme_proxy, sme_server) = |
| create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel"); |
| let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream"); |
| let saved_networks = FakeSavedNetworksManager::new(); |
| let saved_networks_manager = Arc::new(saved_networks); |
| let (telemetry_sender, telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100); |
| let telemetry_sender = TelemetrySender::new(telemetry_sender); |
| let (defect_sender, defect_receiver) = mpsc::unbounded(); |
| let (stats_sender, stats_receiver) = mpsc::unbounded(); |
| let roam_manager = FakeLocalRoamManager::new_with_roam_monitor_stats_sender(stats_sender); |
| let roam_manager = Arc::new(Mutex::new(roam_manager)); |
| |
| TestValues { |
| common_options: CommonStateOptions { |
| proxy: sme_proxy, |
| req_stream: client_req_stream.fuse(), |
| update_sender, |
| saved_networks_manager: saved_networks_manager.clone(), |
| telemetry_sender, |
| iface_id: 1, |
| defect_sender, |
| roam_manager, |
| }, |
| sme_req_stream, |
| saved_networks_manager, |
| client_req_sender, |
| update_receiver, |
| telemetry_receiver, |
| defect_receiver, |
| stats_receiver, |
| } |
| } |
| |
| async fn run_state_machine( |
| fut: impl Future<Output = Result<State, ExitReason>> + Send + 'static, |
| ) { |
| let state_machine = fut.into_state_machine(); |
| select! { |
| _state_machine = state_machine.fuse() => return, |
| } |
| } |
| |
| /// Move stash requests forward so that a save request can progress. |
| fn process_stash_write( |
| exec: &mut fasync::TestExecutor, |
| stash_server: &mut fidl_stash::StoreAccessorRequestStream, |
| ) { |
| assert_variant!( |
| exec.run_until_stalled(&mut stash_server.try_next()), |
| Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::SetValue { .. }))) |
| ); |
| assert_variant!( |
| exec.run_until_stalled(&mut stash_server.try_next()), |
| Poll::Ready(Ok(Some(fidl_stash::StoreAccessorRequest::Flush{responder}))) => { |
| responder.send(Ok(())).expect("failed to send stash response"); |
| } |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn wait_for_connect_result_ignores_other_events() { |
| let mut exec = fasync::TestExecutor::new(); |
| let (connect_txn, remote) = create_proxy::<fidl_sme::ConnectTransactionMarker>().unwrap(); |
| let request_handle = remote.into_stream().unwrap().control_handle(); |
| let response_stream = connect_txn.take_event_stream(); |
| |
| let fut = wait_for_connect_result(response_stream); |
| |
| let mut fut = pin!(fut); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Send some unexpected response |
| let ind = fidl_internal::SignalReportIndication { rssi_dbm: -20, snr_db: 25 }; |
| request_handle.send_on_signal_report(&ind).unwrap(); |
| |
| // Future should still be waiting for OnConnectResult event |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Send expected ConnectResult response |
| let sme_result = fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::Success, |
| is_credential_rejected: false, |
| is_reconnect: false, |
| }; |
| request_handle.send_on_connect_result(&sme_result).unwrap(); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(Ok(response)) => { |
| assert_eq!(sme_result, response); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn wait_for_connect_result_error() { |
| let mut exec = fasync::TestExecutor::new(); |
| let (connect_txn, remote) = create_proxy::<fidl_sme::ConnectTransactionMarker>().unwrap(); |
| let response_stream = connect_txn.take_event_stream(); |
| |
| let fut = wait_for_connect_result(response_stream); |
| |
| let mut fut = pin!(fut); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Drop server end, and verify future completes with error |
| drop(remote); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(Err(ExitReason(_)))); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_successfully_connects() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| // Do SavedNetworksManager set up manually to get functionality and stash server |
| let (saved_networks, mut stash_server) = |
| exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server()); |
| let saved_networks_manager = Arc::new(saved_networks); |
| test_values.common_options.saved_networks_manager = saved_networks_manager.clone(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| // Store the network in the saved_networks_manager, so we can record connection success |
| let save_fut = saved_networks_manager.store( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone(), |
| ); |
| let mut save_fut = pin!(save_fut); |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending); |
| process_stash_write(&mut exec, &mut stash_server); |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None))); |
| |
| // Check that the saved networks manager has the expected initial data |
| let saved_networks = exec.run_singlethreaded( |
| saved_networks_manager.lookup(&connect_selection.target.network.clone()), |
| ); |
| assert_eq!(false, saved_networks[0].has_ever_connected); |
| assert!(saved_networks[0].hidden_probability > 0.0); |
| |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.bss_description, bss_description); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| connect_txn_handle |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| |
| // Check for a connecting update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Connecting, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| process_stash_write(&mut exec, &mut stash_server); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a connect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Connected, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Check that the saved networks manager has the connection recorded |
| let saved_networks = exec.run_singlethreaded( |
| saved_networks_manager.lookup(&connect_selection.target.network.clone()), |
| ); |
| assert_eq!(true, saved_networks[0].has_ever_connected); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure no further updates were sent to listeners |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.update_receiver.into_future()), |
| Poll::Pending |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_times_out() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| // Do SavedNetworksManager set up manually to get functionality and stash server |
| let (saved_networks, mut stash_server) = |
| exec.run_singlethreaded(SavedNetworksManager::new_and_stash_server()); |
| let saved_networks_manager = Arc::new(saved_networks); |
| test_values.common_options.saved_networks_manager = saved_networks_manager.clone(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| // Store the network in the saved_networks_manager |
| let save_fut = saved_networks_manager.store( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone(), |
| ); |
| let mut save_fut = pin!(save_fut); |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Pending); |
| process_stash_write(&mut exec, &mut stash_server); |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None))); |
| |
| // Prepare state machine |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.bss_description, bss_description); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| |
| // Check for a connecting update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Connecting, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Respond with a SignalReport, which should not unblock connecting_state |
| connect_txn_handle |
| .send_on_signal_report(&fidl_internal::SignalReportIndication { |
| rssi_dbm: -25, |
| snr_db: 30, |
| }) |
| .expect("failed to send singal report"); |
| |
| // Run the state machine. Should still be pending |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Wake up the next timer, which is the timeout for the connect request. |
| assert!(exec.wake_next_timer().is_some()); |
| |
| // State machine should exit. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_successfully_scans_and_connects() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(123)); |
| let mut test_values = test_setup(); |
| |
| let connection_attempt_time = fasync::Time::now(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| // Set how the SavedNetworksManager should respond to lookup_compatible for the scan. |
| let expected_config = network_config::NetworkConfig::new( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone(), |
| connect_selection.target.saved_network_info.has_ever_connected, |
| ) |
| .expect("failed to create network config"); |
| test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]); |
| |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| let time_to_connect = 30.seconds(); |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.bss_description, bss_description.clone()); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| exec.set_fake_time(fasync::Time::after(time_to_connect)); |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| connect_txn_handle |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| |
| // Check for a connecting update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Connecting, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a connect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Connected, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Check that the saved networks manager has the connection result recorded |
| assert_variant!(test_values.saved_networks_manager.get_recorded_connect_reslts().as_slice(), [data] => { |
| let expected_connect_result = ConnectResultRecord { |
| id: connect_selection.target.network.clone(), |
| credential: connect_selection.target.credential.clone(), |
| bssid: types::Bssid::from(bss_description.bssid), |
| connect_result: fake_successful_connect_result(), |
| scan_type: connect_selection.target.bss.observation, |
| }; |
| assert_eq!(data, &expected_connect_result); |
| }); |
| |
| // Check that connected telemetry event is sent |
| assert_variant!( |
| test_values.telemetry_receiver.try_next(), |
| Ok(Some(TelemetryEvent::ConnectResult { iface_id: 1, policy_connect_reason, result, multiple_bss_candidates, ap_state, network_is_likely_hidden: _ })) => { |
| assert_eq!(bss_description, ap_state.original().clone().into()); |
| assert_eq!(multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| assert_eq!(policy_connect_reason, Some(connect_selection.reason)); |
| assert_eq!(result, fake_successful_connect_result()); |
| } |
| ); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure no further updates were sent to listeners |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.update_receiver.into_future()), |
| Poll::Pending |
| ); |
| |
| // Send a disconnect and check that the connection data is correctly recorded |
| let is_sme_reconnecting = false; |
| let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting); |
| connect_txn_handle |
| .send_on_disconnect(&fidl_disconnect_info) |
| .expect("failed to send disconnection event"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| let expected_recorded_connection = ConnectionRecord { |
| id: connect_selection.target.network.clone(), |
| credential: connect_selection.target.credential.clone(), |
| data: PastConnectionData { |
| bssid: types::Bssid::from(bss_description.bssid), |
| connection_attempt_time, |
| time_to_connect, |
| disconnect_time: fasync::Time::now(), |
| connection_uptime: zx::Duration::from_minutes(0), |
| disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme, |
| signal_at_disconnect: types::Signal { |
| rssi_dbm: bss_description.rssi_dbm, |
| snr_db: bss_description.snr_db, |
| }, |
| // TODO: record average phy rate over connection once available |
| average_tx_rate: 0, |
| }, |
| }; |
| assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [data] => { |
| assert_eq!(data, &expected_recorded_connection); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_fails_to_connect_and_retries() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| let mut connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.to_vec()); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| let connect_result = fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| ..fake_successful_connect_result() |
| }; |
| connect_txn_handle |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send connection completion"); |
| |
| // Check for a connecting update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: types::NetworkIdentifier { |
| ssid: connect_selection.target.network.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }, |
| state: fidl_policy::ConnectionState::Connecting, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert!(exec.wake_next_timer().is_some()); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check that connect result telemetry event is sent |
| assert_variant!( |
| test_values.telemetry_receiver.try_next(), |
| Ok(Some(TelemetryEvent::ConnectResult { iface_id: 1, policy_connect_reason, result, multiple_bss_candidates, ap_state, network_is_likely_hidden: _ })) => { |
| assert_eq!(bss_description, ap_state.original().clone().into()); |
| assert_eq!(multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| assert_eq!(policy_connect_reason, Some(connect_selection.reason)); |
| assert_eq!(result, connect_result); |
| } |
| ); |
| |
| // Ensure a disconnect request is sent to the SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FailedToConnect }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.to_vec()); |
| assert_eq!(req.bss_description, Sequestered::release(connect_selection.target.bss.bss_description)); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| let connect_result = fake_successful_connect_result(); |
| connect_txn_handle |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send connection completion"); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Empty update sent to NotifyListeners (which in this case, will not actually be sent.) |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks |
| }))) => { |
| assert!(networks.is_empty()); |
| } |
| ); |
| |
| // A defect should be logged. |
| assert_variant!( |
| test_values.defect_receiver.try_next(), |
| Ok(Some(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 }))) |
| ); |
| |
| // Check for a connected update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: types::NetworkIdentifier { |
| ssid: connect_selection.target.network.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }, |
| state: fidl_policy::ConnectionState::Connected, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure no further updates were sent to listeners |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.update_receiver.into_future()), |
| Poll::Pending |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_fails_to_connect_at_max_retries() { |
| let mut exec = fasync::TestExecutor::new(); |
| // Don't use test_values() because of issue with KnownEssStore |
| let (update_sender, mut update_receiver) = mpsc::unbounded(); |
| let (sme_proxy, sme_server) = |
| create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel"); |
| let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream"); |
| let saved_networks_manager = |
| Arc::new(exec.run_singlethreaded(SavedNetworksManager::new_for_test())); |
| let (_client_req_sender, client_req_stream) = mpsc::channel(1); |
| let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100); |
| let telemetry_sender = TelemetrySender::new(telemetry_sender); |
| let (defect_sender, mut defect_receiver) = mpsc::unbounded(); |
| let roam_manager = Arc::new(Mutex::new(FakeLocalRoamManager::new())); |
| |
| let common_options = CommonStateOptions { |
| proxy: sme_proxy, |
| req_stream: client_req_stream.fuse(), |
| update_sender, |
| saved_networks_manager: saved_networks_manager.clone(), |
| telemetry_sender, |
| iface_id: 1, |
| defect_sender, |
| roam_manager, |
| }; |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| // save network to check that failed connect is recorded |
| assert!(exec |
| .run_singlethreaded(saved_networks_manager.store( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone() |
| ),) |
| .expect("Failed to save network") |
| .is_none()); |
| let before_recording = fasync::Time::now(); |
| |
| let connecting_options = ConnectingOptions { |
| connect_selection: connect_selection.clone(), |
| attempt_counter: MAX_CONNECTION_ATTEMPTS - 1, |
| }; |
| let initial_state = connecting_state(common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.bss_description, bss_description.clone()); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| let connect_result = fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| ..fake_successful_connect_result() |
| }; |
| ctrl |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send connection completion"); |
| } |
| ); |
| |
| // After failing to reconnect, the state machine should exit so that the state machine |
| // monitor can attempt to reconnect the interface. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // Check for a connect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Failed, |
| status: Some(fidl_policy::DisconnectStatus::ConnectionFailed), |
| }], |
| }; |
| assert_variant!( |
| update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Check that failure was recorded in SavedNetworksManager |
| let mut configs = exec.run_singlethreaded( |
| saved_networks_manager.lookup(&connect_selection.target.network.clone()), |
| ); |
| let network_config = configs.pop().expect("Failed to get saved network"); |
| let mut failures = |
| network_config.perf_stats.connect_failures.get_recent_for_network(before_recording); |
| let connect_failure = failures.pop().expect("Saved network is missing failure reason"); |
| assert_eq!(connect_failure.reason, FailureReason::GeneralFailure); |
| |
| // A defect should be logged. |
| assert_variant!( |
| defect_receiver.try_next(), |
| Ok(Some(Defect::Iface(IfaceFailure::ConnectionFailure { iface_id: 1 }))) |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_fails_to_connect_with_bad_credentials() { |
| let mut exec = fasync::TestExecutor::new(); |
| // Don't use test_values() because of issue with KnownEssStore |
| let (update_sender, mut update_receiver) = mpsc::unbounded(); |
| let (sme_proxy, sme_server) = |
| create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel"); |
| let sme_req_stream = sme_server.into_stream().expect("could not create SME request stream"); |
| let saved_networks_manager = |
| Arc::new(exec.run_singlethreaded(SavedNetworksManager::new_for_test())); |
| let (_client_req_sender, client_req_stream) = mpsc::channel(1); |
| let (telemetry_sender, _telemetry_receiver) = mpsc::channel::<TelemetryEvent>(100); |
| let telemetry_sender = TelemetrySender::new(telemetry_sender); |
| let (defect_sender, mut defect_receiver) = mpsc::unbounded(); |
| let roam_manager = Arc::new(Mutex::new(FakeLocalRoamManager::new())); |
| |
| let common_options = CommonStateOptions { |
| proxy: sme_proxy, |
| req_stream: client_req_stream.fuse(), |
| update_sender, |
| saved_networks_manager: saved_networks_manager.clone(), |
| telemetry_sender, |
| iface_id: 1, |
| defect_sender, |
| roam_manager, |
| }; |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| // save network to check that failed connect is recorded |
| let saved_networks_manager = common_options.saved_networks_manager.clone(); |
| assert!(exec |
| .run_singlethreaded(saved_networks_manager.store( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone() |
| ),) |
| .expect("Failed to save network") |
| .is_none()); |
| let before_recording = fasync::Time::now(); |
| |
| let connecting_options = ConnectingOptions { |
| connect_selection: connect_selection.clone(), |
| attempt_counter: MAX_CONNECTION_ATTEMPTS - 1, |
| }; |
| let initial_state = connecting_state(common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.bss_description, bss_description.clone()); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| let connect_result = fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| is_credential_rejected: true, |
| ..fake_successful_connect_result() |
| }; |
| ctrl |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send connection completion"); |
| } |
| ); |
| |
| // The state machine should exit when bad credentials are detected so that the state |
| // machine monitor can try to connect to another network. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // Check for a connect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Failed, |
| status: Some(fidl_policy::DisconnectStatus::CredentialsFailed), |
| }], |
| }; |
| assert_variant!( |
| update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Check that failure was recorded in SavedNetworksManager |
| let mut configs = exec.run_singlethreaded( |
| saved_networks_manager.lookup(&connect_selection.target.network.clone()), |
| ); |
| let network_config = configs.pop().expect("Failed to get saved network"); |
| let mut failures = |
| network_config.perf_stats.connect_failures.get_recent_for_network(before_recording); |
| let connect_failure = failures.pop().expect("Saved network is missing failure reason"); |
| assert_eq!(connect_failure.reason, FailureReason::CredentialRejected); |
| |
| // No defect should have been observed. |
| assert_variant!(defect_receiver.try_next(), Ok(None)); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_gets_duplicate_connect_selection() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a connecting update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: types::NetworkIdentifier { |
| ssid: connect_selection.target.network.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }, |
| state: fidl_policy::ConnectionState::Connecting, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Send a duplicate connect request |
| let mut client = Client::new(test_values.client_req_sender); |
| let duplicate_request = types::ConnectSelection { |
| // this incoming request should be deduped regardless of the reason |
| reason: types::ConnectReason::ProactiveNetworkSwitch, |
| ..connect_selection.clone() |
| }; |
| client.connect(duplicate_request).expect("failed to make request"); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a connect request is sent to the SME |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.bss_description, bss_description); |
| assert_eq!(req.multiple_bss_candidates, connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| connect_txn_handle |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a connect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Connected, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure no further updates were sent to listeners |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.update_receiver.into_future()), |
| Poll::Pending |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn connecting_state_has_broken_sme() { |
| let mut exec = fasync::TestExecutor::new(); |
| let test_values = test_setup(); |
| |
| let connect_selection = generate_connect_selection(); |
| |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| |
| // Break the SME by dropping the server end of the SME stream, so it causes an error |
| drop(test_values.sme_req_stream); |
| |
| // Ensure the state machine exits |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_gets_disconnect_request() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let mut test_values = test_setup(); |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let init_ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| let (connect_txn_proxy, _connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connection_attempt_time = fasync::Time::now(); |
| let time_to_connect = zx::Duration::from_seconds(10); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| ap_state: Box::new(init_ap_state.clone()), |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time, |
| time_to_connect, |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| let disconnect_time = fasync::Time::after(12.hours()); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Run forward to get post connection signals metrics |
| exec.set_fake_time(fasync::Time::after(AVERAGE_SCORE_DELTA_MINIMUM_DURATION + 1.second())); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::PostConnectionSignals { .. }); |
| }); |
| |
| // Run forward to get long duration signals metrics |
| exec.set_fake_time(fasync::Time::after(METRICS_SHORT_CONNECT_DURATION + 1.second())); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::LongDurationSignals { .. }); |
| }); |
| |
| // Run forward to disconnect time |
| exec.set_fake_time(disconnect_time); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Send a disconnect request |
| let mut client = Client::new(test_values.client_req_sender); |
| let (sender, mut receiver) = oneshot::channel(); |
| client |
| .disconnect(types::DisconnectReason::FidlStopClientConnectionsRequest, sender) |
| .expect("failed to make request"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Respond to the SME disconnect |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Once the disconnect is processed, the state machine should exit. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // Check for a disconnect update and the responder |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Disconnected, |
| status: Some(fidl_policy::DisconnectStatus::ConnectionStopped), |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(()))); |
| |
| // Disconnect telemetry event sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info } => { |
| assert!(!track_subsequent_downtime); |
| assert_variant!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => { |
| assert_eq!(connected_duration, 12.hours()); |
| assert!(!is_sme_reconnecting); |
| assert_eq!(disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest)); |
| assert_eq!(previous_connect_reason, connect_selection.reason); |
| assert_eq!(ap_state, init_ap_state.clone()); |
| }); |
| }); |
| }); |
| |
| // The disconnect should have been recorded for the saved network config. |
| let expected_recorded_connection = ConnectionRecord { |
| id: connect_selection.target.network.clone(), |
| credential: connect_selection.target.credential.clone(), |
| data: PastConnectionData { |
| bssid: init_ap_state.original().bssid, |
| connection_attempt_time, |
| time_to_connect, |
| disconnect_time, |
| connection_uptime: zx::Duration::from_hours(12), |
| disconnect_reason: types::DisconnectReason::FidlStopClientConnectionsRequest, |
| signal_at_disconnect: types::Signal { |
| rssi_dbm: bss_description.rssi_dbm, |
| snr_db: bss_description.snr_db, |
| }, |
| // TODO: record average phy rate over connection once available |
| average_tx_rate: 0, |
| }, |
| }; |
| assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => { |
| assert_eq!(connection_data, &expected_recorded_connection); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_records_unexpected_disconnect() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let test_values = test_setup(); |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let init_ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| // Save the network in order to later record the disconnect to it. |
| let save_fut = test_values.saved_networks_manager.store( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone(), |
| ); |
| let mut save_fut = pin!(save_fut); |
| assert_variant!(exec.run_until_stalled(&mut save_fut), Poll::Ready(Ok(None))); |
| |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let connection_attempt_time = fasync::Time::now(); |
| let time_to_connect = zx::Duration::from_seconds(10); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| ap_state: Box::new(init_ap_state.clone()), |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time, |
| time_to_connect, |
| }; |
| |
| // Start the state machine in the connected state. |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| let disconnect_time = fasync::Time::after(12.hours()); |
| exec.set_fake_time(disconnect_time); |
| |
| // SME notifies Policy of disconnection |
| let fidl_disconnect_info = generate_disconnect_info(false); |
| connect_txn_handle |
| .send_on_disconnect(&fidl_disconnect_info) |
| .expect("failed to send disconnection event"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // The disconnect should have been recorded for the saved network config. |
| let expected_recorded_connection = ConnectionRecord { |
| id: connect_selection.target.network.clone(), |
| credential: connect_selection.target.credential.clone(), |
| data: PastConnectionData { |
| bssid: init_ap_state.original().bssid, |
| connection_attempt_time, |
| time_to_connect, |
| disconnect_time, |
| connection_uptime: zx::Duration::from_hours(12), |
| disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme, |
| signal_at_disconnect: types::Signal { |
| rssi_dbm: bss_description.rssi_dbm, |
| snr_db: bss_description.snr_db, |
| }, |
| // TODO: record average phy rate over connection once available |
| average_tx_rate: 0, |
| }, |
| }; |
| assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => { |
| assert_eq!(connection_data, &expected_recorded_connection); |
| }); |
| |
| // Disconnect telemetry event sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info } => { |
| assert!(track_subsequent_downtime); |
| assert_variant!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => { |
| assert_eq!(connected_duration, 12.hours()); |
| assert!(!is_sme_reconnecting); |
| assert_eq!(disconnect_source, fidl_disconnect_info.disconnect_source); |
| assert_eq!(previous_connect_reason, connect_selection.reason); |
| assert_eq!(ap_state, init_ap_state); |
| }); |
| }); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_reconnect_resets_connected_duration() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let test_values = test_setup(); |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state.clone()), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| |
| let disconnect_time = fasync::Time::after(12.hours()); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Run forward to get post connection score metrics |
| exec.set_fake_time(fasync::Time::after(AVERAGE_SCORE_DELTA_MINIMUM_DURATION + 1.second())); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::PostConnectionSignals { .. }); |
| }); |
| |
| // Run forward to get long duration signals metrics |
| exec.set_fake_time(fasync::Time::after(METRICS_SHORT_CONNECT_DURATION + 1.second())); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::LongDurationSignals { .. }); |
| }); |
| |
| // Run forward to disconnect time |
| exec.set_fake_time(disconnect_time); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // SME notifies Policy of disconnection with SME-initiated reconnect |
| let is_sme_reconnecting = true; |
| let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting); |
| connect_txn_handle |
| .send_on_disconnect(&fidl_disconnect_info) |
| .expect("failed to send disconnection event"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Disconnect telemetry event sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::Disconnected { info, .. } => { |
| assert_eq!(info.connected_duration, 12.hours()); |
| }); |
| }); |
| |
| // SME notifies Policy of reconnection successful |
| exec.set_fake_time(fasync::Time::after(1.second())); |
| let connect_result = |
| fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() }; |
| connect_txn_handle |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send connect result event"); |
| |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!( |
| telemetry_receiver.try_next(), |
| Ok(Some(TelemetryEvent::ConnectResult { .. })) |
| ); |
| |
| // SME notifies Policy of another disconnection |
| exec.set_fake_time(fasync::Time::after(2.hours())); |
| let is_sme_reconnecting = false; |
| let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting); |
| connect_txn_handle |
| .send_on_disconnect(&fidl_disconnect_info) |
| .expect("failed to send disconnection event"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Another disconnect telemetry event sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::Disconnected { info, .. } => { |
| assert_eq!(info.connected_duration, 2.hours()); |
| }); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_records_unexpected_disconnect_unspecified_bss() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| let connection_attempt_time = fasync::Time::from_nanos(0); |
| exec.set_fake_time(connection_attempt_time); |
| let test_values = test_setup(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| |
| // Setup for network selection in the connecting state to select the intended network. |
| let expected_config = network_config::NetworkConfig::new( |
| connect_selection.target.network.clone(), |
| connect_selection.target.credential.clone(), |
| false, |
| ) |
| .expect("failed to create network config"); |
| test_values.saved_networks_manager.set_lookup_compatible_response(vec![expected_config]); |
| |
| let connecting_options = |
| ConnectingOptions { connect_selection: connect_selection.clone(), attempt_counter: 0 }; |
| let initial_state = connecting_state(test_values.common_options, connecting_options); |
| let state_fut = run_state_machine(initial_state); |
| let mut state_fut = pin!(state_fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending); |
| |
| let time_to_connect = 10.seconds(); |
| exec.set_fake_time(fasync::Time::after(time_to_connect)); |
| |
| // Process connect request sent to SME |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => { |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| connect_txn_handle |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending); |
| |
| // SME notifies Policy of disconnection. |
| let disconnect_time = fasync::Time::after(5.hours()); |
| exec.set_fake_time(disconnect_time); |
| let is_sme_reconnecting = false; |
| connect_txn_handle |
| .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting)) |
| .expect("failed to send disconnection event"); |
| assert_variant!(exec.run_until_stalled(&mut state_fut), Poll::Pending); |
| |
| // The connection data should have been recorded at disconnect. |
| let expected_recorded_connection = ConnectionRecord { |
| id: connect_selection.target.network.clone(), |
| credential: connect_selection.target.credential.clone(), |
| data: PastConnectionData { |
| bssid: types::Bssid::from(bss_description.bssid), |
| connection_attempt_time, |
| time_to_connect, |
| disconnect_time, |
| connection_uptime: zx::Duration::from_hours(5), |
| disconnect_reason: types::DisconnectReason::DisconnectDetectedFromSme, |
| signal_at_disconnect: types::Signal { |
| rssi_dbm: bss_description.rssi_dbm, |
| snr_db: bss_description.snr_db, |
| }, |
| average_tx_rate: 0, |
| }, |
| }; |
| assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => { |
| assert_eq!(connection_data, &expected_recorded_connection); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_gets_duplicate_connect_selection() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| let test_values = test_setup(); |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| let (connect_txn_proxy, _connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state.clone()), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Send another duplicate request |
| let mut client = Client::new(test_values.client_req_sender); |
| client.connect(connect_selection.clone()).expect("failed to make request"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure nothing was sent to the SME |
| assert_variant!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending); |
| |
| // No telemetry event is sent |
| assert_variant!(telemetry_receiver.try_next(), Err(_)); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_gets_different_connect_selection() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let mut test_values = test_setup(); |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| let first_connect_selection = generate_connect_selection(); |
| let first_bss_desc = |
| Sequestered::release(first_connect_selection.target.bss.bss_description.clone()); |
| let first_ap_state = |
| types::ApState::from(BssDescription::try_from(first_bss_desc.clone()).unwrap()); |
| let second_connect_selection = types::ConnectSelection { |
| reason: types::ConnectReason::ProactiveNetworkSwitch, |
| ..generate_connect_selection() |
| }; |
| |
| let (connect_txn_proxy, _connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connection_attempt_time = fasync::Time::now(); |
| let time_to_connect = zx::Duration::from_seconds(10); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: first_connect_selection.clone(), |
| ap_state: Box::new(first_ap_state.clone()), |
| multiple_bss_candidates: first_connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time, |
| time_to_connect, |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| let disconnect_time = fasync::Time::after(12.hours()); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Run forward to get post connection signals metrics |
| exec.set_fake_time(fasync::Time::after(AVERAGE_SCORE_DELTA_MINIMUM_DURATION + 1.second())); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::PostConnectionSignals { .. }); |
| }); |
| |
| // Run forward to get long duration signals metrics |
| exec.set_fake_time(fasync::Time::after(METRICS_SHORT_CONNECT_DURATION + 1.second())); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::LongDurationSignals { .. }); |
| }); |
| |
| // Run forward to disconnect time |
| exec.set_fake_time(disconnect_time); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Send a different connect request |
| let mut client = Client::new(test_values.client_req_sender); |
| client.connect(second_connect_selection.clone()).expect("failed to make request"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // There should be 2 requests to the SME stacked up |
| // First SME request: disconnect |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| // Progress the state machine |
| // TODO(https://fxbug.dev/42130926): remove this once the disconnect request is fire-and-forget |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| // Second SME request: connect to the second network |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, second_connect_selection.target.network.ssid.clone().to_vec()); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| connect_txn_handle |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a disconnect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: first_connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Disconnected, |
| status: Some(fidl_policy::DisconnectStatus::ConnectionStopped), |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Disconnect telemetry event sent |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::Disconnected { track_subsequent_downtime, info } => { |
| assert!(!track_subsequent_downtime); |
| assert_variant!(info, DisconnectInfo {connected_duration, is_sme_reconnecting, disconnect_source, previous_connect_reason, ap_state, ..} => { |
| assert_eq!(connected_duration, 12.hours()); |
| assert!(!is_sme_reconnecting); |
| assert_eq!(disconnect_source, fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch)); |
| assert_eq!(previous_connect_reason, first_connect_selection.reason); |
| assert_eq!(ap_state, first_ap_state.clone()); |
| }); |
| }); |
| }); |
| |
| // Check for a connecting update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: types::NetworkIdentifier { |
| ssid: second_connect_selection.target.network.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }, |
| state: fidl_policy::ConnectionState::Connecting, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| // Check for a connected update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: types::NetworkIdentifier { |
| ssid: second_connect_selection.target.network.ssid.clone(), |
| security_type: types::SecurityType::Wpa2, |
| }, |
| state: fidl_policy::ConnectionState::Connected, |
| status: None, |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure no further updates were sent to listeners |
| assert_variant!( |
| exec.run_until_stalled(&mut test_values.update_receiver.into_future()), |
| Poll::Pending |
| ); |
| |
| // Check that the first connection was recorded |
| let expected_recorded_connection = ConnectionRecord { |
| id: first_connect_selection.target.network.clone(), |
| credential: first_connect_selection.target.credential.clone(), |
| data: PastConnectionData { |
| bssid: types::Bssid::from(first_bss_desc.bssid), |
| connection_attempt_time, |
| time_to_connect, |
| disconnect_time, |
| connection_uptime: zx::Duration::from_hours(12), |
| disconnect_reason: types::DisconnectReason::ProactiveNetworkSwitch, |
| signal_at_disconnect: types::Signal { |
| rssi_dbm: first_bss_desc.rssi_dbm, |
| snr_db: first_bss_desc.snr_db, |
| }, |
| // TODO: record average phy rate over connection once available |
| average_tx_rate: 0, |
| }, |
| }; |
| assert_variant!(test_values.saved_networks_manager.get_recorded_past_connections().as_slice(), [connection_data] => { |
| assert_eq!(connection_data, &expected_recorded_connection); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_notified_of_network_disconnect_no_sme_reconnect_short_uptime_no_retry() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| let test_values = test_setup(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state.clone()), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // SME notifies Policy of disconnection. |
| let is_sme_reconnecting = false; |
| connect_txn_handle |
| .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting)) |
| .expect("failed to send disconnection event"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a disconnect request to SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // The state machine should exit since there is no attempt to reconnect. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_notified_of_network_disconnect_sme_reconnect_successfully() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state.clone()), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // SME notifies Policy of disconnection |
| let is_sme_reconnecting = true; |
| connect_txn_handle |
| .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting)) |
| .expect("failed to send disconnection event"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // SME notifies Policy that reconnects succeeds |
| let connect_result = |
| fidl_sme::ConnectResult { is_reconnect: true, ..fake_successful_connect_result() }; |
| connect_txn_handle |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send reconnection result"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check there were no state updates |
| assert_variant!(test_values.update_receiver.try_next(), Err(_)); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_notified_of_network_disconnect_sme_reconnect_unsuccessfully() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| let mut test_values = test_setup(); |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| // Set the start time of the connection |
| let start_time = fasync::Time::now(); |
| exec.set_fake_time(start_time); |
| |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Set time to indicate a decent uptime before the disconnect so the AP is retried |
| exec.set_fake_time(start_time + fasync::Duration::from_hours(24)); |
| |
| // SME notifies Policy of disconnection |
| let is_sme_reconnecting = true; |
| connect_txn_handle |
| .send_on_disconnect(&generate_disconnect_info(is_sme_reconnecting)) |
| .expect("failed to send disconnection event"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // SME notifies Policy that reconnects fails |
| let connect_result = fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::RefusedReasonUnspecified, |
| is_reconnect: true, |
| ..fake_successful_connect_result() |
| }; |
| connect_txn_handle |
| .send_on_connect_result(&connect_result) |
| .expect("failed to send reconnection result"); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for an SME disconnect request |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // The state machine should exit since there is no policy attempt to reconnect. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // Check for a disconnect update |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Disconnected, |
| status: Some(fidl_policy::DisconnectStatus::ConnectionFailed), |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_on_signal_report() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let mut test_values = test_setup(); |
| |
| // Set initial RSSI and SNR values |
| let mut connect_selection = generate_connect_selection(); |
| let init_rssi = -40; |
| let init_snr = 30; |
| connect_selection.target.bss.signal = |
| types::Signal { rssi_dbm: init_rssi, snr_db: init_snr }; |
| |
| let mut bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| bss_description.rssi_dbm = init_rssi; |
| bss_description.snr_db = init_snr; |
| connect_selection.target.bss.bss_description = bss_description.clone().into(); |
| |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| // Add a PastConnectionData for the connected network to be send in BSS quality data. |
| let mut past_connections = PastConnectionList::default(); |
| let mut past_connection_data = random_connection_data(); |
| past_connection_data.bssid = ieee80211::Bssid::from(bss_description.bssid); |
| past_connections.add(past_connection_data); |
| let mut saved_networks_manager = FakeSavedNetworksManager::new(); |
| saved_networks_manager.past_connections_response = past_connections.clone(); |
| test_values.common_options.saved_networks_manager = Arc::new(saved_networks_manager); |
| |
| // Set up the state machine, starting at the connected state. |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Send the first signal report from SME |
| let rssi_1 = -50; |
| let snr_1 = 25; |
| let fidl_signal_report = |
| fidl_internal::SignalReportIndication { rssi_dbm: rssi_1, snr_db: snr_1 }; |
| connect_txn_handle |
| .send_on_signal_report(&fidl_signal_report) |
| .expect("failed to send signal report"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Do a quick check that state machine does not exist and there's no disconnect to SME |
| assert_variant!(poll_sme_req(&mut exec, &mut sme_fut), Poll::Pending); |
| |
| // Verify that connection stats are sent out |
| assert_variant!(test_values.stats_receiver.try_next(), Ok(Some(stats)) => { |
| assert_eq!(stats.rssi_dbm, rssi_1); |
| assert_eq!(stats.snr_db, snr_1); |
| }); |
| |
| // Send a second signal report with higher RSSI and SNR than the previous reports. |
| let rssi_2 = -30; |
| let snr_2 = 35; |
| let fidl_signal_report = |
| fidl_internal::SignalReportIndication { rssi_dbm: rssi_2, snr_db: snr_2 }; |
| connect_txn_handle |
| .send_on_signal_report(&fidl_signal_report) |
| .expect("failed to send signal report"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Verify that another connection stats is sent out with new signal data. |
| assert_variant!(test_values.stats_receiver.try_next(), Ok(Some(stats)) => { |
| assert_eq!(stats.rssi_dbm, rssi_2); |
| assert_eq!(stats.snr_db, snr_2); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn connected_state_on_channel_switched() { |
| let mut exec = fasync::TestExecutor::new_with_fake_time(); |
| exec.set_fake_time(fasync::Time::from_nanos(0)); |
| |
| let test_values = test_setup(); |
| let mut telemetry_receiver = test_values.telemetry_receiver; |
| |
| let connect_selection = generate_connect_selection(); |
| let bss_description = |
| Sequestered::release(connect_selection.target.bss.bss_description.clone()); |
| let ap_state = |
| types::ApState::from(BssDescription::try_from(bss_description.clone()).unwrap()); |
| |
| // Set up the state machine, starting at the connected state. |
| let (connect_txn_proxy, connect_txn_stream) = |
| create_proxy_and_stream::<fidl_sme::ConnectTransactionMarker>() |
| .expect("failed to create a connect txn channel"); |
| let options = ConnectedOptions { |
| currently_fulfilled_connection: connect_selection.clone(), |
| ap_state: Box::new(ap_state), |
| multiple_bss_candidates: connect_selection.target.network_has_multiple_bss, |
| connect_txn_stream: connect_txn_proxy.take_event_stream(), |
| network_is_likely_hidden: false, |
| connection_attempt_time: fasync::Time::now(), |
| time_to_connect: zx::Duration::from_seconds(10), |
| }; |
| let initial_state = connected_state(test_values.common_options, options); |
| |
| let connect_txn_handle = connect_txn_stream.control_handle(); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| let channel_switch_info = fidl_internal::ChannelSwitchInfo { new_channel: 10 }; |
| connect_txn_handle |
| .send_on_channel_switched(&channel_switch_info) |
| .expect("failed to send signal report"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Verify telemetry event |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::OnChannelSwitched { info } => { |
| assert_eq!(info, channel_switch_info); |
| }); |
| }); |
| |
| // Have SME notify Policy of disconnection so we can see whether the channel in the |
| // BssDescription has changed. |
| let is_sme_reconnecting = false; |
| let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting); |
| connect_txn_handle |
| .send_on_disconnect(&fidl_disconnect_info) |
| .expect("failed to send disconnection event"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Verify telemetry event |
| assert_variant!(telemetry_receiver.try_next(), Ok(Some(event)) => { |
| assert_variant!(event, TelemetryEvent::Disconnected { info, .. } => { |
| assert_eq!(info.ap_state.tracked.channel.primary, 10); |
| }); |
| }); |
| } |
| |
| #[fuchsia::test] |
| fn disconnecting_state_completes_and_exits() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| |
| let (sender, _) = oneshot::channel(); |
| let disconnecting_options = DisconnectingOptions { |
| disconnect_responder: Some(sender), |
| previous_network: None, |
| next_network: None, |
| reason: types::DisconnectReason::RegulatoryRegionChange, |
| }; |
| let initial_state = disconnecting_state(test_values.common_options, disconnecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a disconnect request is sent to the SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::RegulatoryRegionChange }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Ensure the state machine exits once the disconnect is processed. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // The state machine should have sent a listener update |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks |
| }))) => { |
| assert!(networks.is_empty()); |
| } |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn disconnecting_state_completes_disconnect_to_connecting() { |
| let mut exec = fasync::TestExecutor::new(); |
| let mut test_values = test_setup(); |
| |
| let previous_connect_selection = generate_connect_selection(); |
| let next_connect_selection = generate_connect_selection(); |
| |
| let bss_description = |
| Sequestered::release(next_connect_selection.target.bss.bss_description.clone()); |
| |
| let (disconnect_sender, mut disconnect_receiver) = oneshot::channel(); |
| let connecting_options = ConnectingOptions { |
| connect_selection: next_connect_selection.clone(), |
| attempt_counter: 0, |
| }; |
| // Include both a "previous" and "next" network |
| let disconnecting_options = DisconnectingOptions { |
| disconnect_responder: Some(disconnect_sender), |
| previous_network: Some(( |
| previous_connect_selection.target.network.clone(), |
| fidl_policy::DisconnectStatus::ConnectionStopped, |
| )), |
| next_network: Some(connecting_options), |
| reason: types::DisconnectReason::ProactiveNetworkSwitch, |
| }; |
| let initial_state = disconnecting_state(test_values.common_options, disconnecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Run the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Ensure a disconnect request is sent to the SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Progress the state machine |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Check for a disconnect update and the disconnect responder |
| let client_state_update = ClientStateUpdate { |
| state: fidl_policy::WlanClientState::ConnectionsEnabled, |
| networks: vec![ClientNetworkState { |
| id: previous_connect_selection.target.network.clone(), |
| state: fidl_policy::ConnectionState::Disconnected, |
| status: Some(fidl_policy::DisconnectStatus::ConnectionStopped), |
| }], |
| }; |
| assert_variant!( |
| test_values.update_receiver.try_next(), |
| Ok(Some(listener::Message::NotifyListeners(updates))) => { |
| assert_eq!(updates, client_state_update); |
| }); |
| |
| assert_variant!(exec.run_until_stalled(&mut disconnect_receiver), Poll::Ready(Ok(()))); |
| |
| // Ensure a connect request is sent to the SME |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req, txn, control_handle: _ }) => { |
| assert_eq!(req.ssid, next_connect_selection.target.network.ssid.clone().to_vec()); |
| assert_eq!(req.deprecated_scan_type, fidl_fuchsia_wlan_common::ScanType::Active); |
| assert_eq!(req.bss_description, bss_description.clone()); |
| assert_eq!(req.multiple_bss_candidates, next_connect_selection.target.network_has_multiple_bss); |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| } |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn disconnecting_state_has_broken_sme() { |
| let mut exec = fasync::TestExecutor::new(); |
| let test_values = test_setup(); |
| |
| let (sender, mut receiver) = oneshot::channel(); |
| let disconnecting_options = DisconnectingOptions { |
| disconnect_responder: Some(sender), |
| previous_network: None, |
| next_network: None, |
| reason: types::DisconnectReason::NetworkConfigUpdated, |
| }; |
| let initial_state = disconnecting_state(test_values.common_options, disconnecting_options); |
| let fut = run_state_machine(initial_state); |
| let mut fut = pin!(fut); |
| |
| // Break the SME by dropping the server end of the SME stream, so it causes an error |
| drop(test_values.sme_req_stream); |
| |
| // Ensure the state machine exits |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // Expect the responder to have an error |
| assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Err(_))); |
| } |
| |
| #[fuchsia::test] |
| fn serve_loop_handles_startup() { |
| let mut exec = fasync::TestExecutor::new(); |
| let test_values = test_setup(); |
| let sme_proxy = test_values.common_options.proxy; |
| let sme_event_stream = sme_proxy.take_event_stream(); |
| let (_client_req_sender, client_req_stream) = mpsc::channel(1); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Create a connect request so that the state machine does not immediately exit. |
| let connect_selection = generate_connect_selection(); |
| |
| let fut = serve( |
| 0, |
| sme_proxy, |
| sme_event_stream, |
| client_req_stream, |
| test_values.common_options.update_sender, |
| test_values.common_options.saved_networks_manager, |
| Some(connect_selection), |
| test_values.common_options.telemetry_sender, |
| test_values.common_options.defect_sender, |
| test_values.common_options.roam_manager, |
| ); |
| let mut fut = pin!(fut); |
| |
| // Run the state machine so it sends the initial SME disconnect request. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Run the future again and ensure that it has not exited after receiving the response. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| } |
| |
| #[fuchsia::test] |
| fn serve_loop_handles_sme_disappearance() { |
| let mut exec = fasync::TestExecutor::new(); |
| let test_values = test_setup(); |
| let (_client_req_sender, client_req_stream) = mpsc::channel(1); |
| |
| // Make our own SME proxy for this test |
| let (sme_proxy, sme_server) = |
| create_proxy::<fidl_sme::ClientSmeMarker>().expect("failed to create an sme channel"); |
| let (sme_req_stream, sme_control_handle) = sme_server |
| .into_stream_and_control_handle() |
| .expect("could not create SME request stream"); |
| |
| let sme_fut = sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| let sme_event_stream = sme_proxy.take_event_stream(); |
| |
| // Create a connect request so that the state machine does not immediately exit. |
| let connect_selection = generate_connect_selection(); |
| |
| let fut = serve( |
| 0, |
| sme_proxy, |
| sme_event_stream, |
| client_req_stream, |
| test_values.common_options.update_sender, |
| test_values.common_options.saved_networks_manager, |
| Some(connect_selection), |
| test_values.common_options.telemetry_sender, |
| test_values.common_options.defect_sender, |
| test_values.common_options.roam_manager, |
| ); |
| let mut fut = pin!(fut); |
| |
| // Run the state machine so it sends the initial SME disconnect request. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Run the future again and ensure that it has not exited after receiving the response. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| sme_control_handle.shutdown_with_epitaph(zx::Status::UNAVAILABLE); |
| |
| // Ensure the state machine has no further actions and is exited |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| } |
| |
| #[fuchsia::test] |
| fn serve_loop_handles_disconnect() { |
| let mut exec = fasync::TestExecutor::new(); |
| let test_values = test_setup(); |
| let sme_proxy = test_values.common_options.proxy; |
| let sme_event_stream = sme_proxy.take_event_stream(); |
| let (client_req_sender, client_req_stream) = mpsc::channel(1); |
| let sme_fut = test_values.sme_req_stream.into_future(); |
| let mut sme_fut = pin!(sme_fut); |
| |
| // Create a connect request so that the state machine does not immediately exit. |
| let connect_selection = generate_connect_selection(); |
| let fut = serve( |
| 0, |
| sme_proxy, |
| sme_event_stream, |
| client_req_stream, |
| test_values.common_options.update_sender, |
| test_values.common_options.saved_networks_manager, |
| Some(connect_selection), |
| test_values.common_options.telemetry_sender, |
| test_values.common_options.defect_sender, |
| test_values.common_options.roam_manager, |
| ); |
| let mut fut = pin!(fut); |
| |
| // Run the state machine so it sends the initial SME disconnect request. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::Startup }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // Run the future again and ensure that it has not exited after receiving the response. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Absorb the connect request. |
| let connect_txn_handle = assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Connect{ req: _, txn, control_handle: _ }) => { |
| // Send connection response. |
| let (_stream, ctrl) = txn.expect("connect txn unused") |
| .into_stream_and_control_handle().expect("error accessing control handle"); |
| ctrl |
| } |
| ); |
| connect_txn_handle |
| .send_on_connect_result(&fake_successful_connect_result()) |
| .expect("failed to send connection completion"); |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| |
| // Send a disconnect request |
| let mut client = Client::new(client_req_sender); |
| let (sender, mut receiver) = oneshot::channel(); |
| client |
| .disconnect( |
| PolicyDisconnectionMigratedMetricDimensionReason::NetworkConfigUpdated, |
| sender, |
| ) |
| .expect("failed to make request"); |
| |
| // Run the state machine so that it handles the disconnect message. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending); |
| assert_variant!( |
| poll_sme_req(&mut exec, &mut sme_fut), |
| Poll::Ready(fidl_sme::ClientSmeRequest::Disconnect{ responder, reason: fidl_sme::UserDisconnectReason::NetworkConfigUpdated }) => { |
| responder.send().expect("could not send sme response"); |
| } |
| ); |
| |
| // The state machine should exit following the disconnect request. |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| |
| // Expect the responder to be acknowledged |
| assert_variant!(exec.run_until_stalled(&mut receiver), Poll::Ready(Ok(()))); |
| } |
| |
| #[fuchsia::test] |
| fn serve_loop_handles_state_machine_error() { |
| let mut exec = fasync::TestExecutor::new(); |
| let test_values = test_setup(); |
| let sme_proxy = test_values.common_options.proxy; |
| let sme_event_stream = sme_proxy.take_event_stream(); |
| let (_client_req_sender, client_req_stream) = mpsc::channel(1); |
| |
| // Create a connect request so that the state machine does not immediately exit. |
| let connect_selection = generate_connect_selection(); |
| |
| let fut = serve( |
| 0, |
| sme_proxy, |
| sme_event_stream, |
| client_req_stream, |
| test_values.common_options.update_sender, |
| test_values.common_options.saved_networks_manager, |
| Some(connect_selection), |
| test_values.common_options.telemetry_sender, |
| test_values.common_options.defect_sender, |
| test_values.common_options.roam_manager, |
| ); |
| let mut fut = pin!(fut); |
| |
| // Drop the server end of the SME stream, so it causes an error |
| drop(test_values.sme_req_stream); |
| |
| // Ensure the state machine exits |
| assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(())); |
| } |
| |
| fn fake_successful_connect_result() -> fidl_sme::ConnectResult { |
| fidl_sme::ConnectResult { |
| code: fidl_ieee80211::StatusCode::Success, |
| is_credential_rejected: false, |
| is_reconnect: false, |
| } |
| } |
| } |