Skip to main content

fdf_component/
incoming.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::marker::PhantomData;
6
7use anyhow::{Context, Error, anyhow};
8use cm_types::{IterablePath, RelativePath};
9use fdf_sys::fdf_token_transfer;
10use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, ServiceMarker, ServiceProxy};
11use fidl_fuchsia_io as fio;
12use fidl_fuchsia_io::Flags;
13use fidl_next_bind::Service;
14use fuchsia_component::client::{Connect, connect_to_service_instance_at_dir_svc};
15use fuchsia_component::directory::{AsRefDirectory, Directory, open_directory_async};
16use fuchsia_component::{DEFAULT_SERVICE_INSTANCE, SVC_DIR};
17use log::error;
18use namespace::{Entry, Namespace};
19use zx::Status;
20
21/// Implements access to the incoming namespace for a driver. It provides methods
22/// for accessing incoming protocols and services by either their marker or proxy
23/// types, and can be used as a [`Directory`] with the functions in
24/// [`fuchsia_component::client`].
25pub struct Incoming(Vec<Entry>);
26
27impl Incoming {
28    /// Connects to the protocol in the service instance's path in the incoming namespace. Logs and
29    /// returns a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
30    pub fn connect_protocol<T: Connect>(&self) -> Result<T, Status> {
31        T::connect_at_dir_svc(&self).map_err(|e| {
32            error!(
33                "Failed to connect to discoverable protocol `{}`: {e}",
34                T::Protocol::PROTOCOL_NAME
35            );
36            Status::CONNECTION_REFUSED
37        })
38    }
39
40    /// Connects to the protocol in the service instance's path in the given directory, with
41    /// zx::Channel. Logs and returns a [`Status::CONNECTION_REFUSED`] if the service instance
42    /// couldn't be opened.
43    pub fn connect_protocol_next<P: fidl_next::Discoverable, D>(
44        &self,
45    ) -> Result<fidl_next::ClientEnd<P, libasync_fidl::AsyncChannel<D>>, Status>
46    where
47        D: Default,
48    {
49        let path = format!("/svc/{}", P::PROTOCOL_NAME);
50        Self::connect_protocol_next_at(self, &path)
51    }
52
53    /// Connects to the protocol in the service instance's path in the given directory, with
54    /// zx::Channel. Logs and returns a [`Status::CONNECTION_REFUSED`] if the service instance
55    /// couldn't be opened.
56    pub fn connect_protocol_next_at<P: fidl_next::Discoverable, D>(
57        dir: &impl AsRefDirectory,
58        path: &str,
59    ) -> Result<fidl_next::ClientEnd<P, libasync_fidl::AsyncChannel<D>>, Status>
60    where
61        D: Default,
62    {
63        let (client_end, server_end) = zx::Channel::create();
64        let client_end = fidl_next::ClientEnd::<P, zx::Channel>::from_untyped(client_end);
65        dir.as_ref_directory().open(path, fio::Flags::PROTOCOL_SERVICE, server_end).map_err(
66            |e| {
67                error!("Failed to connect to discoverable protocol `{}`: {e}", P::PROTOCOL_NAME);
68                Status::CONNECTION_REFUSED
69            },
70        )?;
71        Ok(libasync_fidl::AsyncChannel::<D>::client_from_zx_channel::<P>(client_end))
72    }
73
74    /// Connects to the protocol in the service instance's path in the given directory over driver
75    /// transport, with [`fdf_fidl::DriverChannel`], using `dispatcher`. Logs and returns a
76    /// [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
77    pub fn connect_protocol_driver_transport<P: fidl_next::Discoverable, D>(
78        &self,
79        dispatcher: D,
80    ) -> Result<fidl_next::ClientEnd<P, fdf_fidl::DriverChannel<D>>, zx::Status>
81    where
82        D: Clone,
83    {
84        let path = format!("/svc/{}", P::PROTOCOL_NAME);
85        Self::connect_protocol_driver_transport_at(self, &path, dispatcher)
86    }
87
88    /// Connects to the protocol in the service instance's path in the given directory over driver
89    /// transport, with [`fdf_fidl::DriverChannel`], using `dispatcher`. Logs and returns a
90    /// [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
91    pub fn connect_protocol_driver_transport_at<P: fidl_next::Discoverable, D>(
92        dir: &impl AsRefDirectory,
93        path: &str,
94        dispatcher: D,
95    ) -> Result<fidl_next::ClientEnd<P, fdf_fidl::DriverChannel<D>>, zx::Status>
96    where
97        D: Clone,
98    {
99        let (client_token, server_token) = zx::Channel::create();
100        let (client_end, server_end) = fdf_fidl::DriverChannel::create_with_dispatcher(dispatcher);
101
102        dir.as_ref_directory().open(path, fio::Flags::PROTOCOL_SERVICE, server_token).map_err(
103            |e| {
104                error!("Failed to connect to discoverable protocol `{}`: {e}", P::PROTOCOL_NAME);
105                zx::Status::CONNECTION_REFUSED
106            },
107        )?;
108        // SAFETY: client_token and server_end are valid by construction and
109        // `fdf_token_transfer` consumes both handles and does not interact with rust memory.
110        zx::Status::ok(unsafe {
111            fdf_sys::fdf_token_transfer(
112                client_token.into_raw(),
113                server_end.into_driver_handle().into_raw().get(),
114            )
115        })
116        .inspect_err(|e| {
117            error!("Failed to connect to discoverable protocol `{}`: {e}", P::PROTOCOL_NAME);
118        })?;
119
120        Ok(fidl_next::ClientEnd::<P, _>::from_untyped(client_end))
121    }
122
123    /// Creates a connector to the given service's default instance by its marker type. This can be
124    /// convenient when the compiler can't deduce the [`ServiceProxy`] type on its own.
125    ///
126    /// See [`ServiceConnector`] for more about what you can do with the connector.
127    ///
128    /// # Example
129    ///
130    /// ```ignore
131    /// let service = context.incoming.service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker).connect()?;
132    /// let device = service.connect_to_device()?;
133    /// ```
134    pub fn service_marker<M: ServiceMarker>(&self, _marker: M) -> ServiceConnector<'_, M::Proxy> {
135        ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData }
136    }
137
138    /// Creates a connector to the given service's default instance by its proxy type. This can be
139    /// convenient when the compiler can deduce the [`ServiceProxy`] type on its own.
140    ///
141    /// See [`ServiceConnector`] for more about what you can do with the connector.
142    ///
143    /// # Example
144    ///
145    /// ```ignore
146    /// struct MyProxies {
147    ///     i2c_service: fidl_fuchsia_hardware_i2c::ServiceProxy,
148    /// }
149    /// let proxies = MyProxies {
150    ///     i2c_service: context.incoming.service().connect()?;
151    /// };
152    /// ```
153    pub fn service<P>(&self) -> ServiceConnector<'_, P> {
154        ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData }
155    }
156}
157
158impl From<Vec<cm_types::NamespaceEntry>> for Incoming {
159    fn from(other: Vec<cm_types::NamespaceEntry>) -> Self {
160        Self(other.into_iter().map(Into::into).collect())
161    }
162}
163
164/// A builder for connecting to an aggregated service instance in the driver's incoming namespace.
165/// By default, it will connect to the default instance, named `default`. You can override this
166/// by calling [`Self::instance`].
167pub struct ServiceConnector<'incoming, ServiceProxy> {
168    incoming: &'incoming Incoming,
169    instance: &'incoming str,
170    _p: PhantomData<ServiceProxy>,
171}
172
173impl<'a, S> ServiceConnector<'a, S> {
174    /// Overrides the instance name to connect to when [`Self::connect`] is called.
175    pub fn instance(self, instance: &'a str) -> Self {
176        let Self { incoming, _p, .. } = self;
177        Self { incoming, instance, _p }
178    }
179}
180
181impl<'a, S: ServiceProxy> ServiceConnector<'a, S>
182where
183    S::Service: ServiceMarker,
184{
185    /// Connects to the service instance's path in the incoming namespace. Logs and returns
186    /// a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
187    pub fn connect(self) -> Result<S, Status> {
188        connect_to_service_instance_at_dir_svc::<S::Service>(self.incoming, self.instance).map_err(
189            |e| {
190                error!(
191                    "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}",
192                    S::Service::SERVICE_NAME,
193                    self.instance
194                );
195                Status::CONNECTION_REFUSED
196            },
197        )
198    }
199}
200
201/// Used with [`ServiceHandlerAdapter`] as a connector to members of a service instance.
202pub struct ServiceMemberConnector(fio::DirectoryProxy);
203
204fn connect(
205    dir: &fio::DirectoryProxy,
206    member: &str,
207    server_end: zx::Channel,
208) -> Result<(), fidl::Error> {
209    #[cfg(fuchsia_api_level_at_least = "27")]
210    return dir.open(member, fio::Flags::PROTOCOL_SERVICE, &fio::Options::default(), server_end);
211    #[cfg(not(fuchsia_api_level_at_least = "27"))]
212    return dir.open3(member, fio::Flags::PROTOCOL_SERVICE, &fio::Options::default(), server_end);
213}
214
215impl fidl_next_protocol::ServiceConnector<zx::Channel> for ServiceMemberConnector {
216    type Error = fidl::Error;
217    fn connect_to_member(&self, member: &str, server_end: zx::Channel) -> Result<(), Self::Error> {
218        connect(&self.0, member, server_end)
219    }
220}
221
222impl fidl_next_protocol::ServiceConnector<fdf_fidl::DriverChannel> for ServiceMemberConnector {
223    type Error = Status;
224    fn connect_to_member(
225        &self,
226        member: &str,
227        server_end: fdf_fidl::DriverChannel,
228    ) -> Result<(), Self::Error> {
229        let (client_token, server_token) = zx::Channel::create();
230
231        // SAFETY: client_token and server_end are valid by construction and `fdf_token_transfer`
232        // consumes both handles and does not interact with rust memory.
233        Status::ok(unsafe {
234            fdf_token_transfer(
235                client_token.into_raw(),
236                server_end.into_driver_handle().into_raw().get(),
237            )
238        })?;
239
240        connect(&self.0, member, server_token).map_err(|err| {
241            error!("Failed to connect to service member {member}: {err:?}");
242            Status::CONNECTION_REFUSED
243        })
244    }
245}
246
247/// A type alias representing a service instance with members that can be connected to using the
248/// [`fidl_next`] bindings.
249pub type ServiceInstance<S> = fidl_next_bind::ServiceConnector<S, ServiceMemberConnector>;
250
251impl<'a, S: Service<ServiceMemberConnector>> ServiceConnector<'a, ServiceInstance<S>> {
252    /// Connects to the service instance's path in the incoming namespace with the new wire bindings.
253    /// Logs and returns a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
254    pub fn connect_next(self) -> Result<ServiceInstance<S>, Status> {
255        let service_path = format!("{SVC_DIR}/{}/{}", S::SERVICE_NAME, self.instance);
256        let dir = open_directory_async(self.incoming, &service_path, fio::Rights::empty())
257            .map_err(|e| {
258                error!(
259                    "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}",
260                    S::SERVICE_NAME,
261                    self.instance
262                );
263                Status::CONNECTION_REFUSED
264            })?;
265        Ok(fidl_next_bind::ServiceConnector::from_untyped(ServiceMemberConnector(dir)))
266    }
267}
268
269impl From<Namespace> for Incoming {
270    fn from(value: Namespace) -> Self {
271        Incoming(value.flatten())
272    }
273}
274
275impl From<ClientEnd<fio::DirectoryMarker>> for Incoming {
276    fn from(client: ClientEnd<fio::DirectoryMarker>) -> Self {
277        Incoming(vec![Entry {
278            path: cm_types::NamespacePath::new("/").unwrap(),
279            directory: client,
280        }])
281    }
282}
283
284/// Returns the remainder of a prefix match of `prefix` against `self` in terms of path segments.
285///
286/// For example:
287/// ```ignore
288/// match_prefix("pkg/data", "pkg") == Some("/data")
289/// match_prefix("pkg_data", "pkg") == None
290/// ```
291fn match_prefix(match_in: &impl IterablePath, prefix: &impl IterablePath) -> Option<RelativePath> {
292    let mut my_segments = match_in.iter_segments();
293    let mut prefix_segments = prefix.iter_segments();
294    for prefix in prefix_segments.by_ref() {
295        if prefix != my_segments.next()? {
296            return None;
297        }
298    }
299    if prefix_segments.next().is_some() {
300        // did not match all prefix segments
301        return None;
302    }
303    let segments = Vec::from_iter(my_segments);
304    Some(RelativePath::from(segments))
305}
306
307impl Directory for Incoming {
308    fn open(&self, path: &str, flags: Flags, server_end: zx::Channel) -> Result<(), Error> {
309        let path = path.strip_prefix("/").unwrap_or(path);
310        let path = RelativePath::new(path)?;
311
312        for entry in &self.0 {
313            if let Some(remain) = match_prefix(&path, &entry.path) {
314                return entry.directory.open(&format!("{}", remain), flags, server_end);
315            }
316        }
317        Err(Status::NOT_FOUND)
318            .with_context(|| anyhow!("Path {path} not found in incoming namespace"))
319    }
320}
321
322impl AsRefDirectory for Incoming {
323    fn as_ref_directory(&self) -> &dyn Directory {
324        self
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use fuchsia_async::Task;
332    use fuchsia_component::server::ServiceFs;
333    use futures::stream::StreamExt;
334
335    enum IncomingServices {
336        Device(fidl_fuchsia_hardware_i2c::DeviceRequestStream),
337        DefaultService(fidl_fuchsia_hardware_i2c::ServiceRequest),
338        OtherService(fidl_fuchsia_hardware_i2c::ServiceRequest),
339    }
340
341    impl IncomingServices {
342        async fn handle_device_stream(
343            stream: fidl_fuchsia_hardware_i2c::DeviceRequestStream,
344            name: &str,
345        ) {
346            stream
347                .for_each(|msg| async move {
348                    match msg.unwrap() {
349                        fidl_fuchsia_hardware_i2c::DeviceRequest::GetName { responder } => {
350                            responder.send(Ok(name)).unwrap();
351                        }
352                        _ => unimplemented!(),
353                    }
354                })
355                .await
356        }
357
358        async fn handle(self) {
359            use IncomingServices::*;
360            match self {
361                Device(stream) => Self::handle_device_stream(stream, "device").await,
362                DefaultService(fidl_fuchsia_hardware_i2c::ServiceRequest::Device(stream)) => {
363                    Self::handle_device_stream(stream, "default").await
364                }
365                OtherService(fidl_fuchsia_hardware_i2c::ServiceRequest::Device(stream)) => {
366                    Self::handle_device_stream(stream, "other").await
367                }
368            }
369        }
370    }
371
372    async fn make_incoming() -> Incoming {
373        let (client, server) = fidl::endpoints::create_endpoints();
374        let mut fs = ServiceFs::new();
375        fs.dir("svc")
376            .add_fidl_service(IncomingServices::Device)
377            .add_fidl_service_instance("default", IncomingServices::DefaultService)
378            .add_fidl_service_instance("other", IncomingServices::OtherService);
379        fs.serve_connection(server).expect("error serving handle");
380
381        Task::spawn(fs.for_each_concurrent(100, IncomingServices::handle)).detach_on_drop();
382        Incoming::from(client)
383    }
384
385    #[fuchsia::test]
386    async fn protocol_connect_present() -> anyhow::Result<()> {
387        let incoming = make_incoming().await;
388        // try a protocol that we did set up
389        incoming
390            .connect_protocol::<fidl_fuchsia_hardware_i2c::DeviceProxy>()?
391            .get_name()
392            .await?
393            .unwrap();
394        Ok(())
395    }
396
397    #[fuchsia::test]
398    async fn protocol_connect_not_present() -> anyhow::Result<()> {
399        let incoming = make_incoming().await;
400        // try one we didn't
401        incoming
402            .connect_protocol::<fidl_fuchsia_hwinfo::DeviceProxy>()?
403            .get_info()
404            .await
405            .unwrap_err();
406        Ok(())
407    }
408
409    #[fuchsia::test]
410    async fn service_connect_default_instance() -> anyhow::Result<()> {
411        let incoming = make_incoming().await;
412        // try the default service instance that we did set up
413        assert_eq!(
414            "default",
415            &incoming
416                .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
417                .connect()?
418                .connect_to_device()?
419                .get_name()
420                .await?
421                .unwrap()
422        );
423        assert_eq!(
424            "default",
425            &incoming
426                .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
427                .connect()?
428                .connect_to_device()?
429                .get_name()
430                .await?
431                .unwrap()
432        );
433        Ok(())
434    }
435
436    #[fuchsia::test]
437    async fn service_connect_other_instance() -> anyhow::Result<()> {
438        let incoming = make_incoming().await;
439        // try the other service instance that we did set up
440        assert_eq!(
441            "other",
442            &incoming
443                .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
444                .instance("other")
445                .connect()?
446                .connect_to_device()?
447                .get_name()
448                .await?
449                .unwrap()
450        );
451        assert_eq!(
452            "other",
453            &incoming
454                .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
455                .instance("other")
456                .connect()?
457                .connect_to_device()?
458                .get_name()
459                .await?
460                .unwrap()
461        );
462        Ok(())
463    }
464
465    #[fuchsia::test]
466    async fn service_connect_invalid_instance() -> anyhow::Result<()> {
467        let incoming = make_incoming().await;
468        // try the invalid service instance that we did not set up
469        incoming
470            .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
471            .instance("invalid")
472            .connect()?
473            .connect_to_device()?
474            .get_name()
475            .await
476            .unwrap_err();
477        incoming
478            .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
479            .instance("invalid")
480            .connect()?
481            .connect_to_device()?
482            .get_name()
483            .await
484            .unwrap_err();
485        Ok(())
486    }
487}