| // 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. |
| |
| //! Extensions for fuchsia.net.interfaces.admin. |
| |
| use fidl::endpoints::ProtocolMarker as _; |
| use fidl::{HandleBased, Rights}; |
| use fidl_fuchsia_net_interfaces as fnet_interfaces; |
| use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin; |
| use fuchsia_zircon_status as zx; |
| use futures::{Future, FutureExt as _, Stream, StreamExt as _, TryStreamExt as _}; |
| use thiserror::Error; |
| |
| /// Error type when using a [`fnet_interfaces_admin::AddressStateProviderProxy`]. |
| #[derive(Error, Debug)] |
| pub enum AddressStateProviderError { |
| /// Address removed error. |
| #[error("address removed: {0:?}")] |
| AddressRemoved(fnet_interfaces_admin::AddressRemovalReason), |
| /// FIDL error. |
| #[error("fidl error")] |
| Fidl(#[from] fidl::Error), |
| /// Channel closed. |
| #[error("AddressStateProvider channel closed")] |
| ChannelClosed, |
| } |
| |
| impl From<TerminalError<fnet_interfaces_admin::AddressRemovalReason>> |
| for AddressStateProviderError |
| { |
| fn from(e: TerminalError<fnet_interfaces_admin::AddressRemovalReason>) -> Self { |
| match e { |
| TerminalError::Fidl(e) => AddressStateProviderError::Fidl(e), |
| TerminalError::Terminal(r) => AddressStateProviderError::AddressRemoved(r), |
| } |
| } |
| } |
| |
| /// Waits for the `OnAddressAdded` event to be received on the event stream. |
| /// |
| /// Returns an error if an address removed event is received instead. |
| pub async fn wait_for_address_added_event( |
| event_stream: &mut fnet_interfaces_admin::AddressStateProviderEventStream, |
| ) -> Result<(), AddressStateProviderError> { |
| let event = event_stream |
| .next() |
| .await |
| .ok_or(AddressStateProviderError::ChannelClosed)? |
| .map_err(AddressStateProviderError::Fidl)?; |
| match event { |
| fnet_interfaces_admin::AddressStateProviderEvent::OnAddressAdded {} => Ok(()), |
| fnet_interfaces_admin::AddressStateProviderEvent::OnAddressRemoved { error } => { |
| Err(AddressStateProviderError::AddressRemoved(error)) |
| } |
| } |
| } |
| |
| // TODO(https://fxbug.dev/42162477): Introduce type with better concurrency safety |
| // for hanging gets. |
| /// Returns a stream of assignment states obtained by watching on `address_state_provider`. |
| /// |
| /// Note that this function calls the hanging get FIDL method |
| /// [`AddressStateProviderProxy::watch_address_assignment_state`] internally, |
| /// which means that this stream should not be polled concurrently with any |
| /// logic which calls the same hanging get. This also means that callers should |
| /// be careful not to drop the returned stream when it has been polled but yet |
| /// to yield an item, e.g. due to a timeout or if using select with another |
| /// stream, as doing so causes a pending hanging get to get lost, and may cause |
| /// future hanging get calls to fail or the channel to be closed. |
| pub fn assignment_state_stream( |
| address_state_provider: fnet_interfaces_admin::AddressStateProviderProxy, |
| ) -> impl Stream<Item = Result<fnet_interfaces::AddressAssignmentState, AddressStateProviderError>> |
| { |
| let event_fut = address_state_provider |
| .take_event_stream() |
| .filter_map(|event| { |
| futures::future::ready(match event { |
| Ok(event) => match event { |
| fnet_interfaces_admin::AddressStateProviderEvent::OnAddressAdded {} => None, |
| fnet_interfaces_admin::AddressStateProviderEvent::OnAddressRemoved { |
| error, |
| } => Some(AddressStateProviderError::AddressRemoved(error)), |
| }, |
| Err(e) => Some(AddressStateProviderError::Fidl(e)), |
| }) |
| }) |
| .into_future() |
| .map(|(event, _stream)| event.unwrap_or(AddressStateProviderError::ChannelClosed)); |
| futures::stream::try_unfold( |
| (address_state_provider, event_fut), |
| |(address_state_provider, event_fut)| { |
| // NB: Rely on the fact that select always polls the left future |
| // first to guarantee that if a terminal event was yielded by the |
| // right future, then we don't have an assignment state to emit to |
| // clients. |
| futures::future::select( |
| address_state_provider.watch_address_assignment_state(), |
| event_fut, |
| ) |
| .then(|s| match s { |
| futures::future::Either::Left((state_result, event_fut)) => match state_result { |
| Ok(state) => { |
| futures::future::ok(Some((state, (address_state_provider, event_fut)))) |
| .left_future() |
| } |
| Err(e) if e.is_closed() => event_fut.map(Result::Err).right_future(), |
| Err(e) => { |
| futures::future::err(AddressStateProviderError::Fidl(e)).left_future() |
| } |
| }, |
| futures::future::Either::Right((error, _state_fut)) => { |
| futures::future::err(error).left_future() |
| } |
| }) |
| }, |
| ) |
| } |
| |
| // TODO(https://fxbug.dev/42162477): Introduce type with better concurrency safety |
| // for hanging gets. |
| /// Wait until the Assigned state is observed on `stream`. |
| /// |
| /// After this async function resolves successfully, the underlying |
| /// `AddressStateProvider` may be used as usual. If an error is returned, a |
| /// terminal error has occurred on the underlying channel. |
| pub async fn wait_assignment_state<S>( |
| stream: S, |
| want: fnet_interfaces::AddressAssignmentState, |
| ) -> Result<(), AddressStateProviderError> |
| where |
| S: Stream<Item = Result<fnet_interfaces::AddressAssignmentState, AddressStateProviderError>> |
| + Unpin, |
| { |
| stream |
| .try_filter_map(|state| futures::future::ok((state == want).then(|| ()))) |
| .try_next() |
| .await |
| .and_then(|opt| opt.ok_or_else(|| AddressStateProviderError::ChannelClosed)) |
| } |
| |
| type ControlEventStreamFutureToReason = |
| fn( |
| ( |
| Option<Result<fnet_interfaces_admin::ControlEvent, fidl::Error>>, |
| fnet_interfaces_admin::ControlEventStream, |
| ), |
| ) -> Result<Option<fnet_interfaces_admin::InterfaceRemovedReason>, fidl::Error>; |
| |
| /// Convert [`fnet_interfaces_admin::GrantForInterfaceAuthorization`] to |
| /// [`fnet_interfaces_admin::ProofOfInterfaceAuthorization`] with fewer |
| /// permissions. |
| /// |
| /// # Panics |
| /// |
| /// Panics when the Event handle does not have the DUPLICATE right. Callers |
| /// need not worry about this if providing a grant received from |
| /// [`GetAuthorizationForInterface`]. |
| pub fn proof_from_grant( |
| grant: &fnet_interfaces_admin::GrantForInterfaceAuthorization, |
| ) -> fnet_interfaces_admin::ProofOfInterfaceAuthorization { |
| let fnet_interfaces_admin::GrantForInterfaceAuthorization { interface_id, token } = grant; |
| |
| // The handle duplication is expected to succeed since the input |
| // `GrantFromInterfaceAuthorization` is retrieved directly from FIDL and has |
| // `zx::Rights::DUPLICATE`. Failure may occur if memory is limited, but this |
| // problem cannot be easily resolved via userspace. |
| fnet_interfaces_admin::ProofOfInterfaceAuthorization { |
| interface_id: *interface_id, |
| token: token.duplicate_handle(Rights::TRANSFER).unwrap(), |
| } |
| } |
| |
| /// A wrapper for fuchsia.net.interfaces.admin/Control that observes terminal |
| /// events. |
| #[derive(Clone)] |
| pub struct Control { |
| proxy: fnet_interfaces_admin::ControlProxy, |
| // Keeps a shared future that will resolve when the first event is seen on a |
| // ControlEventStream. The shared future makes the observed terminal event |
| // "sticky" for as long as we clone the future before polling it. Note that |
| // we don't drive the event stream to completion, the future is resolved |
| // when the first event is seen. That means this relies on the terminal |
| // event contract but does *not* enforce that the channel is closed |
| // immediately after or that no other events are issued. |
| terminal_event_fut: futures::future::Shared< |
| futures::future::Map< |
| futures::stream::StreamFuture<fnet_interfaces_admin::ControlEventStream>, |
| ControlEventStreamFutureToReason, |
| >, |
| >, |
| } |
| |
| /// Waits for response on query result and terminal event. If the query has a |
| /// result, returns that. Otherwise, returns the terminal event. |
| async fn or_terminal_event<QR, QF, TR>( |
| query_fut: QF, |
| terminal_event_fut: TR, |
| ) -> Result<QR, TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>> |
| where |
| QR: Unpin, |
| QF: Unpin + Future<Output = Result<QR, fidl::Error>>, |
| TR: Unpin |
| + Future<Output = Result<Option<fnet_interfaces_admin::InterfaceRemovedReason>, fidl::Error>>, |
| { |
| match futures::future::select(query_fut, terminal_event_fut).await { |
| futures::future::Either::Left((query_result, terminal_event_fut)) => match query_result { |
| Ok(ok) => Ok(ok), |
| Err(e) if e.is_closed() => match terminal_event_fut.await { |
| Ok(Some(reason)) => Err(TerminalError::Terminal(reason)), |
| Ok(None) | Err(_) => Err(TerminalError::Fidl(e)), |
| }, |
| Err(e) => Err(TerminalError::Fidl(e)), |
| }, |
| futures::future::Either::Right((event, query_fut)) => { |
| // We need to poll the query response future one more time, |
| // because of the following scenario: |
| // |
| // 1. select() polls the query response future, which returns |
| // pending. |
| // 2. The server sends the query response and terminal event in |
| // that order. |
| // 3. The FIDL client library dequeues both of these and wakes |
| // the respective futures. |
| // 4. select() polls the terminal event future, which is now |
| // ready. |
| // |
| // In that case, both futures will be ready, so we can use |
| // now_or_never() to check whether the query result future has a |
| // result, since we always want to process that result first. |
| if let Some(query_result) = query_fut.now_or_never() { |
| match query_result { |
| Ok(ok) => Ok(ok), |
| Err(e) if e.is_closed() => match event { |
| Ok(Some(reason)) => Err(TerminalError::Terminal(reason)), |
| Ok(None) | Err(_) => Err(TerminalError::Fidl(e)), |
| }, |
| Err(e) => Err(TerminalError::Fidl(e)), |
| } |
| } else { |
| match event.map_err(|e| TerminalError::Fidl(e))? { |
| Some(removal_reason) => Err(TerminalError::Terminal(removal_reason)), |
| None => Err(TerminalError::Fidl(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| })), |
| } |
| } |
| } |
| } |
| } |
| |
| impl Control { |
| /// Calls `AddAddress` on the proxy. |
| pub fn add_address( |
| &self, |
| address: &fidl_fuchsia_net::Subnet, |
| parameters: &fnet_interfaces_admin::AddressParameters, |
| address_state_provider: fidl::endpoints::ServerEnd< |
| fnet_interfaces_admin::AddressStateProviderMarker, |
| >, |
| ) -> Result<(), TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>> { |
| self.or_terminal_event_no_return(self.proxy.add_address( |
| address, |
| parameters, |
| address_state_provider, |
| )) |
| } |
| |
| /// Calls `GetId` on the proxy. |
| pub async fn get_id( |
| &self, |
| ) -> Result<u64, TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>> { |
| self.or_terminal_event(self.proxy.get_id()).await |
| } |
| |
| /// Calls `RemoveAddress` on the proxy. |
| pub async fn remove_address( |
| &self, |
| address: &fidl_fuchsia_net::Subnet, |
| ) -> Result< |
| fnet_interfaces_admin::ControlRemoveAddressResult, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.remove_address(address)).await |
| } |
| |
| /// Calls `SetConfiguration` on the proxy. |
| pub async fn set_configuration( |
| &self, |
| config: &fnet_interfaces_admin::Configuration, |
| ) -> Result< |
| fnet_interfaces_admin::ControlSetConfigurationResult, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.set_configuration(config)).await |
| } |
| |
| /// Calls `GetConfiguration` on the proxy. |
| pub async fn get_configuration( |
| &self, |
| ) -> Result< |
| fnet_interfaces_admin::ControlGetConfigurationResult, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.get_configuration()).await |
| } |
| |
| /// Calls `GetAuthorizationForInterface` on the proxy. |
| pub async fn get_authorization_for_interface( |
| &self, |
| ) -> Result< |
| fnet_interfaces_admin::GrantForInterfaceAuthorization, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.get_authorization_for_interface()).await |
| } |
| |
| /// Calls `Enable` on the proxy. |
| pub async fn enable( |
| &self, |
| ) -> Result< |
| fnet_interfaces_admin::ControlEnableResult, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.enable()).await |
| } |
| |
| /// Calls `Remove` on the proxy. |
| pub async fn remove( |
| &self, |
| ) -> Result< |
| fnet_interfaces_admin::ControlRemoveResult, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.remove()).await |
| } |
| |
| /// Calls `Disable` on the proxy. |
| pub async fn disable( |
| &self, |
| ) -> Result< |
| fnet_interfaces_admin::ControlDisableResult, |
| TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>, |
| > { |
| self.or_terminal_event(self.proxy.disable()).await |
| } |
| |
| /// Calls `Detach` on the proxy. |
| pub fn detach( |
| &self, |
| ) -> Result<(), TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>> { |
| self.or_terminal_event_no_return(self.proxy.detach()) |
| } |
| |
| /// Creates a new `Control` wrapper from `proxy`. |
| pub fn new(proxy: fnet_interfaces_admin::ControlProxy) -> Self { |
| let terminal_event_fut = proxy |
| .take_event_stream() |
| .into_future() |
| .map::<_, ControlEventStreamFutureToReason>(|(event, _stream)| { |
| event |
| .map(|r| { |
| r.map( |
| |fnet_interfaces_admin::ControlEvent::OnInterfaceRemoved { reason }| { |
| reason |
| }, |
| ) |
| }) |
| .transpose() |
| }) |
| .shared(); |
| Self { proxy, terminal_event_fut } |
| } |
| |
| /// Waits for interface removal. |
| pub async fn wait_termination( |
| self, |
| ) -> TerminalError<fnet_interfaces_admin::InterfaceRemovedReason> { |
| let Self { proxy: _, terminal_event_fut } = self; |
| match terminal_event_fut.await { |
| Ok(Some(event)) => TerminalError::Terminal(event), |
| Ok(None) => TerminalError::Fidl(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| }), |
| Err(e) => TerminalError::Fidl(e), |
| } |
| } |
| |
| /// Creates a new `Control` and its `ServerEnd`. |
| pub fn create_endpoints( |
| ) -> Result<(Self, fidl::endpoints::ServerEnd<fnet_interfaces_admin::ControlMarker>), fidl::Error> |
| { |
| let (proxy, server_end) = fidl::endpoints::create_proxy()?; |
| Ok((Self::new(proxy), server_end)) |
| } |
| |
| async fn or_terminal_event<R: Unpin, F: Unpin + Future<Output = Result<R, fidl::Error>>>( |
| &self, |
| fut: F, |
| ) -> Result<R, TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>> { |
| or_terminal_event(fut, self.terminal_event_fut.clone()).await |
| } |
| |
| fn or_terminal_event_no_return( |
| &self, |
| r: Result<(), fidl::Error>, |
| ) -> Result<(), TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>> { |
| r.map_err(|err| { |
| if !err.is_closed() { |
| return TerminalError::Fidl(err); |
| } |
| // TODO(https://fxbug.dev/42178907): The terminal event may have been |
| // sent by the server but the future may not resolve immediately, |
| // resulting in the terminal event being missed and a FIDL error |
| // being returned to the user. |
| // |
| // Poll event stream to see if we have a terminal event to return |
| // instead of a FIDL closed error. |
| match self.terminal_event_fut.clone().now_or_never() { |
| Some(Ok(Some(terminal_event))) => TerminalError::Terminal(terminal_event), |
| Some(Err(e)) => { |
| // Prefer the error observed by the proxy. |
| let _: fidl::Error = e; |
| TerminalError::Fidl(err) |
| } |
| None | Some(Ok(None)) => TerminalError::Fidl(err), |
| } |
| }) |
| } |
| } |
| |
| impl std::fmt::Debug for Control { |
| fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let Self { proxy, terminal_event_fut: _ } = self; |
| fmt.debug_struct("Control").field("proxy", proxy).finish() |
| } |
| } |
| |
| /// Errors observed from wrapped terminal events. |
| #[derive(Debug)] |
| pub enum TerminalError<E> { |
| /// Terminal event was observed. |
| Terminal(E), |
| /// A FIDL error occurred. |
| Fidl(fidl::Error), |
| } |
| |
| impl<E> std::fmt::Display for TerminalError<E> |
| where |
| E: std::fmt::Debug, |
| { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| match self { |
| TerminalError::Terminal(e) => write!(f, "terminal event: {:?}", e), |
| TerminalError::Fidl(e) => write!(f, "fidl error: {}", e), |
| } |
| } |
| } |
| |
| impl<E: std::fmt::Debug> std::error::Error for TerminalError<E> {} |
| |
| #[cfg(test)] |
| mod test { |
| use std::task::Poll; |
| |
| use super::{ |
| assignment_state_stream, or_terminal_event, proof_from_grant, AddressStateProviderError, |
| TerminalError, |
| }; |
| use assert_matches::assert_matches; |
| use fidl::prelude::*; |
| use fidl::Rights; |
| use fidl_fuchsia_net_interfaces as fnet_interfaces; |
| use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin; |
| use fnet_interfaces_admin::InterfaceRemovedReason; |
| use fuchsia_zircon_status as zx; |
| use futures::{FutureExt as _, StreamExt as _, TryStreamExt as _}; |
| use test_case::test_case; |
| |
| // Test that the terminal event is observed when the server closes its end. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_assignment_state_stream() { |
| let (address_state_provider, server_end) = |
| fidl::endpoints::create_proxy::<fnet_interfaces_admin::AddressStateProviderMarker>() |
| .expect("failed to create proxy"); |
| let state_stream = assignment_state_stream(address_state_provider); |
| futures::pin_mut!(state_stream); |
| |
| const REMOVAL_REASON_INVALID: fnet_interfaces_admin::AddressRemovalReason = |
| fnet_interfaces_admin::AddressRemovalReason::Invalid; |
| { |
| let (mut request_stream, control_handle) = server_end |
| .into_stream_and_control_handle() |
| .expect("failed to create stream and control handle"); |
| |
| const ASSIGNMENT_STATE_ASSIGNED: fnet_interfaces::AddressAssignmentState = |
| fnet_interfaces::AddressAssignmentState::Assigned; |
| let state_fut = state_stream.try_next().map(|r| { |
| assert_eq!( |
| r.expect("state stream error").expect("state stream ended"), |
| ASSIGNMENT_STATE_ASSIGNED |
| ) |
| }); |
| let handle_fut = request_stream.try_next().map(|r| match r.expect("request stream error").expect("request stream ended") { |
| fnet_interfaces_admin::AddressStateProviderRequest::WatchAddressAssignmentState { responder } => { |
| let () = responder.send(ASSIGNMENT_STATE_ASSIGNED).expect("failed to send stubbed assignment state"); |
| } |
| req => panic!("unexpected method called: {:?}", req), |
| }); |
| let ((), ()) = futures::join!(state_fut, handle_fut); |
| |
| let () = control_handle |
| .send_on_address_removed(REMOVAL_REASON_INVALID) |
| .expect("failed to send fake INVALID address removal reason event"); |
| } |
| |
| assert_matches::assert_matches!( |
| state_stream.try_collect::<Vec<_>>().await, |
| Err(AddressStateProviderError::AddressRemoved(got)) if got == REMOVAL_REASON_INVALID |
| ); |
| } |
| |
| // Test that only one error is returned on the assignment state stream when |
| // an error observable on both the client proxy and the event stream occurs. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_assignment_state_stream_single_error() { |
| let (address_state_provider, server_end) = |
| fidl::endpoints::create_proxy::<fnet_interfaces_admin::AddressStateProviderMarker>() |
| .expect("failed to create proxy"); |
| let state_stream = assignment_state_stream(address_state_provider); |
| |
| let () = server_end |
| .close_with_epitaph(fidl::Status::INTERNAL) |
| .expect("failed to send INTERNAL epitaph"); |
| |
| // Use collect rather than try_collect to ensure that we don't observe |
| // multiple errors on this stream. |
| assert_matches::assert_matches!( |
| state_stream |
| .collect::<Vec<_>>() |
| .now_or_never() |
| .expect("state stream not immediately ready") |
| .as_slice(), |
| [Err(AddressStateProviderError::Fidl(fidl::Error::ClientChannelClosed { |
| status: fidl::Status::INTERNAL, |
| protocol_name: _, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None |
| }))] |
| ); |
| } |
| |
| // Test that if an assignment state and a terminal event is available at |
| // the same time, the state is yielded first. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn assignment_state_stream_state_before_event() { |
| let (address_state_provider, mut request_stream) = |
| fidl::endpoints::create_proxy_and_stream::< |
| fnet_interfaces_admin::AddressStateProviderMarker, |
| >() |
| .expect("failed to create proxy"); |
| |
| const ASSIGNMENT_STATE_ASSIGNED: fnet_interfaces::AddressAssignmentState = |
| fnet_interfaces::AddressAssignmentState::Assigned; |
| const REMOVAL_REASON_INVALID: fnet_interfaces_admin::AddressRemovalReason = |
| fnet_interfaces_admin::AddressRemovalReason::Invalid; |
| |
| let ((), ()) = futures::future::join( |
| async move { |
| let () = request_stream |
| .try_next() |
| .await |
| .expect("request stream error") |
| .expect("request stream ended") |
| .into_watch_address_assignment_state() |
| .expect("unexpected request") |
| .send(ASSIGNMENT_STATE_ASSIGNED) |
| .expect("failed to send stubbed assignment state"); |
| let () = request_stream |
| .control_handle() |
| .send_on_address_removed(REMOVAL_REASON_INVALID) |
| .expect("failed to send fake INVALID address removal reason event"); |
| }, |
| async move { |
| let got = assignment_state_stream(address_state_provider).collect::<Vec<_>>().await; |
| assert_matches::assert_matches!( |
| got.as_slice(), |
| &[ |
| Ok(got_state), |
| Err(AddressStateProviderError::AddressRemoved(got_reason)), |
| ] => { |
| assert_eq!(got_state, ASSIGNMENT_STATE_ASSIGNED); |
| assert_eq!(got_reason, REMOVAL_REASON_INVALID); |
| } |
| ); |
| }, |
| ) |
| .await; |
| } |
| |
| // Tests that terminal event is observed when using ControlWrapper. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn control_terminal_event() { |
| let (control, mut request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_interfaces_admin::ControlMarker>() |
| .expect("create proxy"); |
| let control = super::Control::new(control); |
| const EXPECTED_EVENT: fnet_interfaces_admin::InterfaceRemovedReason = |
| fnet_interfaces_admin::InterfaceRemovedReason::BadPort; |
| const ID: u64 = 15; |
| let ((), ()) = futures::future::join( |
| async move { |
| assert_matches::assert_matches!(control.get_id().await, Ok(ID)); |
| assert_matches::assert_matches!( |
| control.get_id().await, |
| Err(super::TerminalError::Terminal(got)) if got == EXPECTED_EVENT |
| ); |
| }, |
| async move { |
| let responder = request_stream |
| .try_next() |
| .await |
| .expect("operating request stream") |
| .expect("stream ended unexpectedly") |
| .into_get_id() |
| .expect("unexpected request"); |
| let () = responder.send(ID).expect("failed to send response"); |
| let () = request_stream |
| .control_handle() |
| .send_on_interface_removed(EXPECTED_EVENT) |
| .expect("sending terminal event"); |
| }, |
| ) |
| .await; |
| } |
| |
| // Tests that terminal error is observed when using ControlWrapper if no |
| // event is issued. |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn control_missing_terminal_event() { |
| let (control, mut request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_interfaces_admin::ControlMarker>() |
| .expect("create proxy"); |
| let control = super::Control::new(control); |
| let ((), ()) = futures::future::join( |
| async move { |
| assert_matches::assert_matches!( |
| control.get_id().await, |
| Err(super::TerminalError::Fidl(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fidl_fuchsia_net_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None |
| })) |
| ); |
| }, |
| async move { |
| match request_stream |
| .try_next() |
| .await |
| .expect("operating request stream") |
| .expect("stream ended unexpectedly") |
| { |
| fnet_interfaces_admin::ControlRequest::GetId { responder } => { |
| // Just close the channel without issuing a response. |
| std::mem::drop(responder); |
| } |
| request => panic!("unexpected request {:?}", request), |
| } |
| }, |
| ) |
| .await; |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn control_pipelined_error() { |
| let (control, request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_interfaces_admin::ControlMarker>() |
| .expect("create proxy"); |
| let control = super::Control::new(control); |
| const CLOSE_REASON: fnet_interfaces_admin::InterfaceRemovedReason = |
| fnet_interfaces_admin::InterfaceRemovedReason::BadPort; |
| let () = request_stream |
| .control_handle() |
| .send_on_interface_removed(CLOSE_REASON) |
| .expect("send terminal event"); |
| std::mem::drop(request_stream); |
| assert_matches::assert_matches!(control.or_terminal_event_no_return(Ok(())), Ok(())); |
| assert_matches::assert_matches!( |
| control |
| .or_terminal_event_no_return(Err(fidl::Error::ClientWrite(zx::Status::INTERNAL))), |
| Err(super::TerminalError::Fidl(fidl::Error::ClientWrite(zx::Status::INTERNAL))) |
| ); |
| #[cfg(target_os = "fuchsia")] |
| assert_matches::assert_matches!( |
| control.or_terminal_event_no_return(Err(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| })), |
| Err(super::TerminalError::Terminal(CLOSE_REASON)) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn control_wait_termination() { |
| let (control, request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_interfaces_admin::ControlMarker>() |
| .expect("create proxy"); |
| let control = super::Control::new(control); |
| const CLOSE_REASON: fnet_interfaces_admin::InterfaceRemovedReason = |
| fnet_interfaces_admin::InterfaceRemovedReason::BadPort; |
| let () = request_stream |
| .control_handle() |
| .send_on_interface_removed(CLOSE_REASON) |
| .expect("send terminal event"); |
| std::mem::drop(request_stream); |
| assert_matches::assert_matches!( |
| control.wait_termination().await, |
| super::TerminalError::Terminal(CLOSE_REASON) |
| ); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn control_respond_and_drop() { |
| const ID: u64 = 15; |
| let (control, mut request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_interfaces_admin::ControlMarker>() |
| .expect("create proxy"); |
| let control = super::Control::new(control); |
| let ((), ()) = futures::future::join( |
| async move { |
| assert_matches::assert_matches!(control.get_id().await, Ok(ID)); |
| }, |
| async move { |
| let responder = request_stream |
| .try_next() |
| .await |
| .expect("operating request stream") |
| .expect("stream ended unexpectedly") |
| .into_get_id() |
| .expect("unexpected request"); |
| let () = responder.send(ID).expect("failed to send response"); |
| }, |
| ) |
| .await; |
| } |
| |
| // This test is for the case found in https://fxbug.dev/328297563. The |
| // query result and terminal event futures both become ready after the query |
| // result is polled and returns pending. This test does not handle the case |
| // for when there is no query result. |
| #[test_case(Ok(()), Ok(Some(InterfaceRemovedReason::User)), Ok(()); "success")] |
| #[test_case( |
| Err(fidl::Error::InvalidHeader), |
| Ok(Some(InterfaceRemovedReason::User)), |
| Err(TerminalError::Fidl(fidl::Error::InvalidHeader)); |
| "returns query error when not closed" |
| )] |
| #[test_case( |
| Err(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| }), |
| Ok(Some(InterfaceRemovedReason::User)), |
| Err(TerminalError::Terminal(InterfaceRemovedReason::User)); |
| "returns terminal error when channel closed" |
| )] |
| #[test_case( |
| Err(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| }), |
| Ok(None), |
| Err(TerminalError::Fidl( |
| fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| } |
| )); |
| "returns query error when no terminal error" |
| )] |
| #[test_case( |
| Err(fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| }), |
| Err(fidl::Error::InvalidHeader), |
| Err(TerminalError::Fidl( |
| fidl::Error::ClientChannelClosed { |
| status: zx::Status::PEER_CLOSED, |
| protocol_name: fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| #[cfg(not(target_os = "fuchsia"))] |
| reason: None, |
| } |
| )); |
| "returns query error when terminal event returns a fidl error" |
| )] |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn control_polling_race( |
| left_future_result: Result<(), fidl::Error>, |
| right_future_result: Result< |
| Option<fnet_interfaces_admin::InterfaceRemovedReason>, |
| fidl::Error, |
| >, |
| expected: Result<(), TerminalError<fnet_interfaces_admin::InterfaceRemovedReason>>, |
| ) { |
| let mut polled = false; |
| let first_future = std::future::poll_fn(|_cx| { |
| if polled { |
| Poll::Ready(left_future_result.clone()) |
| } else { |
| polled = true; |
| Poll::Pending |
| } |
| }) |
| .fuse(); |
| |
| let second_future = |
| std::future::poll_fn(|_cx| Poll::Ready(right_future_result.clone())).fuse(); |
| |
| let res = or_terminal_event(first_future, second_future).await; |
| match (res, expected) { |
| (Ok(()), Ok(())) => (), |
| (Err(TerminalError::Terminal(res)), Err(TerminalError::Terminal(expected))) |
| if res == expected => {} |
| // fidl::Error doesn't implement Eq, but this lack of an actual |
| // equality check does not matter for this test. |
| (Err(TerminalError::Fidl(_)), Err(TerminalError::Fidl(_))) => (), |
| (res, expected) => panic!("expected {:?} got {:?}", expected, res), |
| } |
| } |
| |
| #[test] |
| fn convert_proof_to_grant() { |
| // The default Event has more Rights than the token within the Grant returned from |
| // [`GetAuthorizationForInterface`], but can still be converted to be used in the |
| // [`ProofOfInterfaceAuthorization`], since only `zx::Rights::DUPLICATE` and |
| // `zx::Rights::TRANSFER` is required. |
| let event = fidl::Event::create(); |
| let grant = fnet_interfaces_admin::GrantForInterfaceAuthorization { |
| interface_id: Default::default(), |
| token: event, |
| }; |
| |
| let fnet_interfaces_admin::ProofOfInterfaceAuthorization { interface_id, token } = |
| proof_from_grant(&grant); |
| assert_eq!(interface_id, Default::default()); |
| assert_matches!(token.basic_info(), Ok(info) if info.rights == Rights::TRANSFER); |
| } |
| } |