| // Copyright 2024 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 std::marker::PhantomData; |
| |
| use anyhow::{anyhow, Context, Error}; |
| use cm_types::{IterablePath, RelativePath}; |
| use fdf_sys::fdf_token_transfer; |
| use fidl::endpoints::{DiscoverableProtocolMarker, ServiceMarker, ServiceProxy}; |
| use fidl_fuchsia_io as fio; |
| use fidl_fuchsia_io::Flags; |
| use fidl_next_bind::Service; |
| use fuchsia_component::client::{connect_to_service_instance_at_dir_svc, Connect}; |
| use fuchsia_component::directory::{open_directory_async, AsRefDirectory, Directory}; |
| use fuchsia_component::{DEFAULT_SERVICE_INSTANCE, SVC_DIR}; |
| use log::error; |
| use namespace::{Entry, Namespace}; |
| use zx::{HandleBased, Status}; |
| |
| /// Implements access to the incoming namespace for a driver. It provides methods |
| /// for accessing incoming protocols and services by either their marker or proxy |
| /// types, and can be used as a [`Directory`] with the functions in |
| /// [`fuchsia_component::client`]. |
| pub struct Incoming(Vec<Entry>); |
| |
| impl Incoming { |
| /// Connects to the protocol in the service instance's path in the incoming namespace. Logs and |
| /// returns a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened. |
| pub fn connect_protocol<T: Connect>(&self) -> Result<T, Status> { |
| T::connect_at_dir_svc(&self).map_err(|e| { |
| error!( |
| "Failed to connect to discoverable protocol `{}`: {e}", |
| T::Protocol::PROTOCOL_NAME |
| ); |
| Status::CONNECTION_REFUSED |
| }) |
| } |
| |
| /// Creates a connector to the given service's default instance by its marker type. This can be |
| /// convenient when the compiler can't deduce the [`ServiceProxy`] type on its own. |
| /// |
| /// See [`ServiceConnector`] for more about what you can do with the connector. |
| /// |
| /// # Example |
| /// |
| /// ```ignore |
| /// let service = context.incoming.service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker).connect()?; |
| /// let device = service.connect_to_device()?; |
| /// ``` |
| pub fn service_marker<M: ServiceMarker>(&self, _marker: M) -> ServiceConnector<'_, M::Proxy> { |
| ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData } |
| } |
| |
| /// Creates a connector to the given service's default instance by its proxy type. This can be |
| /// convenient when the compiler can deduce the [`ServiceProxy`] type on its own. |
| /// |
| /// See [`ServiceConnector`] for more about what you can do with the connector. |
| /// |
| /// # Example |
| /// |
| /// ```ignore |
| /// struct MyProxies { |
| /// i2c_service: fidl_fuchsia_hardware_i2c::ServiceProxy, |
| /// } |
| /// let proxies = MyProxies { |
| /// i2c_service: context.incoming.service().connect()?; |
| /// }; |
| /// ``` |
| pub fn service<P>(&self) -> ServiceConnector<'_, P> { |
| ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData } |
| } |
| } |
| |
| /// A builder for connecting to an aggregated service instance in the driver's incoming namespace. |
| /// By default, it will connect to the default instance, named `default`. You can override this |
| /// by calling [`Self::instance`]. |
| pub struct ServiceConnector<'incoming, ServiceProxy> { |
| incoming: &'incoming Incoming, |
| instance: &'incoming str, |
| _p: PhantomData<ServiceProxy>, |
| } |
| |
| impl<'a, S> ServiceConnector<'a, S> { |
| /// Overrides the instance name to connect to when [`Self::connect`] is called. |
| pub fn instance(self, instance: &'a str) -> Self { |
| let Self { incoming, _p, .. } = self; |
| Self { incoming, instance, _p } |
| } |
| } |
| |
| impl<'a, S: ServiceProxy> ServiceConnector<'a, S> |
| where |
| S::Service: ServiceMarker, |
| { |
| /// Connects to the service instance's path in the incoming namespace. Logs and returns |
| /// a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened. |
| pub fn connect(self) -> Result<S, Status> { |
| connect_to_service_instance_at_dir_svc::<S::Service>(self.incoming, self.instance).map_err( |
| |e| { |
| error!( |
| "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}", |
| S::Service::SERVICE_NAME, |
| self.instance |
| ); |
| Status::CONNECTION_REFUSED |
| }, |
| ) |
| } |
| } |
| |
| /// Used with [`ServiceHandlerAdapter`] as a connector to members of a service instance. |
| pub struct ServiceMemberConnector(fio::DirectoryProxy); |
| |
| fn connect( |
| dir: &fio::DirectoryProxy, |
| member: &str, |
| server_end: zx::Channel, |
| ) -> Result<(), fidl::Error> { |
| #[cfg(fuchsia_api_level_at_least = "27")] |
| return dir.open(member, fio::Flags::PROTOCOL_SERVICE, &fio::Options::default(), server_end); |
| #[cfg(not(fuchsia_api_level_at_least = "27"))] |
| return dir.open3(member, fio::Flags::PROTOCOL_SERVICE, &fio::Options::default(), server_end); |
| } |
| |
| impl fidl_next_protocol::ServiceConnector<zx::Channel> for ServiceMemberConnector { |
| type Error = fidl::Error; |
| fn connect_to_member(&self, member: &str, server_end: zx::Channel) -> Result<(), Self::Error> { |
| connect(&self.0, member, server_end) |
| } |
| } |
| |
| impl fidl_next_protocol::ServiceConnector<fdf_fidl::DriverChannel> for ServiceMemberConnector { |
| type Error = Status; |
| fn connect_to_member( |
| &self, |
| member: &str, |
| server_end: fdf_fidl::DriverChannel, |
| ) -> Result<(), Self::Error> { |
| let (client_token, server_token) = zx::Channel::create(); |
| connect(&self.0, member, server_token).map_err(|err| { |
| error!("Failed to connect to service member {member}: {err:?}"); |
| Status::CONNECTION_REFUSED |
| })?; |
| // SAFETY: client_token and server_end are valid by construction and `fdf_token_transfer` |
| // consumes both handles and does not interact with rust memory. |
| Status::ok(unsafe { |
| fdf_token_transfer( |
| client_token.into_raw(), |
| server_end.into_driver_handle().into_raw().get(), |
| ) |
| }) |
| } |
| } |
| |
| /// A type alias representing a service instance with members that can be connected to using the |
| /// [`fidl_next`] bindings. |
| pub type ServiceInstance<S> = fidl_next_bind::ServiceConnector<S, ServiceMemberConnector>; |
| |
| impl<'a, S: Service<ServiceMemberConnector>> ServiceConnector<'a, ServiceInstance<S>> { |
| /// Connects to the service instance's path in the incoming namespace with the new wire bindings. |
| /// Logs and returns a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened. |
| pub fn connect_next(self) -> Result<ServiceInstance<S>, Status> { |
| let service_path = format!("{SVC_DIR}/{}/{}", S::SERVICE_NAME, self.instance); |
| let dir = open_directory_async(self.incoming, &service_path, fio::Rights::empty()) |
| .map_err(|e| { |
| error!( |
| "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}", |
| S::SERVICE_NAME, |
| self.instance |
| ); |
| Status::CONNECTION_REFUSED |
| })?; |
| Ok(fidl_next_bind::ServiceConnector::from_untyped(ServiceMemberConnector(dir))) |
| } |
| } |
| |
| impl From<Namespace> for Incoming { |
| fn from(value: Namespace) -> Self { |
| Incoming(value.flatten()) |
| } |
| } |
| |
| /// Returns the remainder of a prefix match of `prefix` against `self` in terms of path segments. |
| /// |
| /// For example: |
| /// ```ignore |
| /// match_prefix("pkg/data", "pkg") == Some("/data") |
| /// match_prefix("pkg_data", "pkg") == None |
| /// ``` |
| fn match_prefix(match_in: &impl IterablePath, prefix: &impl IterablePath) -> Option<RelativePath> { |
| let mut my_segments = match_in.iter_segments(); |
| let mut prefix_segments = prefix.iter_segments(); |
| while let Some(prefix) = prefix_segments.next() { |
| if prefix != my_segments.next()? { |
| return None; |
| } |
| } |
| if prefix_segments.next().is_some() { |
| // did not match all prefix segments |
| return None; |
| } |
| let segments = Vec::from_iter(my_segments); |
| Some(RelativePath::from(segments)) |
| } |
| |
| impl Directory for Incoming { |
| fn open(&self, path: &str, flags: Flags, server_end: zx::Channel) -> Result<(), Error> { |
| let path = path.strip_prefix("/").unwrap_or(path); |
| let path = RelativePath::new(path)?; |
| |
| for entry in &self.0 { |
| if let Some(remain) = match_prefix(&path, &entry.path) { |
| return entry.directory.open(&format!("/{}", remain), flags, server_end); |
| } |
| } |
| Err(Status::NOT_FOUND) |
| .with_context(|| anyhow!("Path {path} not found in incoming namespace")) |
| } |
| } |
| |
| impl AsRefDirectory for Incoming { |
| fn as_ref_directory(&self) -> &dyn Directory { |
| self |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use cm_types::NamespacePath; |
| use fuchsia_async::Task; |
| use fuchsia_component::server::ServiceFs; |
| use futures::stream::StreamExt; |
| |
| enum IncomingServices { |
| I2cDevice(fidl_fuchsia_hardware_i2c::DeviceRequestStream), |
| I2cDefaultService(fidl_fuchsia_hardware_i2c::ServiceRequest), |
| I2cOtherService(fidl_fuchsia_hardware_i2c::ServiceRequest), |
| } |
| |
| impl IncomingServices { |
| async fn handle_device_stream( |
| stream: fidl_fuchsia_hardware_i2c::DeviceRequestStream, |
| name: &str, |
| ) { |
| stream |
| .for_each(|msg| async move { |
| match msg.unwrap() { |
| fidl_fuchsia_hardware_i2c::DeviceRequest::GetName { responder } => { |
| responder.send(Ok(name)).unwrap(); |
| } |
| _ => unimplemented!(), |
| } |
| }) |
| .await |
| } |
| |
| async fn handle(self) { |
| use fidl_fuchsia_hardware_i2c::ServiceRequest::*; |
| use IncomingServices::*; |
| match self { |
| I2cDevice(stream) => Self::handle_device_stream(stream, "device").await, |
| I2cDefaultService(Device(stream)) => { |
| Self::handle_device_stream(stream, "default").await |
| } |
| I2cOtherService(Device(stream)) => { |
| Self::handle_device_stream(stream, "other").await |
| } |
| } |
| } |
| } |
| |
| async fn make_incoming() -> Incoming { |
| let (client, server) = fidl::endpoints::create_endpoints(); |
| let mut fs = ServiceFs::new(); |
| fs.dir("svc") |
| .add_fidl_service(IncomingServices::I2cDevice) |
| .add_fidl_service_instance("default", IncomingServices::I2cDefaultService) |
| .add_fidl_service_instance("other", IncomingServices::I2cOtherService); |
| fs.serve_connection(server).expect("error serving handle"); |
| |
| Task::spawn(fs.for_each_concurrent(100, IncomingServices::handle)).detach_on_drop(); |
| |
| Incoming(vec![Entry { path: NamespacePath::new("/").unwrap(), directory: client }]) |
| } |
| |
| #[fuchsia::test] |
| async fn protocol_connect_present() -> anyhow::Result<()> { |
| let incoming = make_incoming().await; |
| // try a protocol that we did set up |
| incoming |
| .connect_protocol::<fidl_fuchsia_hardware_i2c::DeviceProxy>()? |
| .get_name() |
| .await? |
| .unwrap(); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn protocol_connect_not_present() -> anyhow::Result<()> { |
| let incoming = make_incoming().await; |
| // try one we didn't |
| incoming |
| .connect_protocol::<fidl_fuchsia_hwinfo::DeviceProxy>()? |
| .get_info() |
| .await |
| .unwrap_err(); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn service_connect_default_instance() -> anyhow::Result<()> { |
| let incoming = make_incoming().await; |
| // try the default service instance that we did set up |
| assert_eq!( |
| "default", |
| &incoming |
| .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker) |
| .connect()? |
| .connect_to_device()? |
| .get_name() |
| .await? |
| .unwrap() |
| ); |
| assert_eq!( |
| "default", |
| &incoming |
| .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>() |
| .connect()? |
| .connect_to_device()? |
| .get_name() |
| .await? |
| .unwrap() |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn service_connect_other_instance() -> anyhow::Result<()> { |
| let incoming = make_incoming().await; |
| // try the other service instance that we did set up |
| assert_eq!( |
| "other", |
| &incoming |
| .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker) |
| .instance("other") |
| .connect()? |
| .connect_to_device()? |
| .get_name() |
| .await? |
| .unwrap() |
| ); |
| assert_eq!( |
| "other", |
| &incoming |
| .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>() |
| .instance("other") |
| .connect()? |
| .connect_to_device()? |
| .get_name() |
| .await? |
| .unwrap() |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn service_connect_invalid_instance() -> anyhow::Result<()> { |
| let incoming = make_incoming().await; |
| // try the invalid service instance that we did not set up |
| incoming |
| .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker) |
| .instance("invalid") |
| .connect()? |
| .connect_to_device()? |
| .get_name() |
| .await |
| .unwrap_err(); |
| incoming |
| .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>() |
| .instance("invalid") |
| .connect()? |
| .connect_to_device()? |
| .get_name() |
| .await |
| .unwrap_err(); |
| Ok(()) |
| } |
| } |