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    /// Creates a connector to the given service's default instance by its marker type. This can be
75    /// convenient when the compiler can't deduce the [`ServiceProxy`] type on its own.
76    ///
77    /// See [`ServiceConnector`] for more about what you can do with the connector.
78    ///
79    /// # Example
80    ///
81    /// ```ignore
82    /// let service = context.incoming.service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker).connect()?;
83    /// let device = service.connect_to_device()?;
84    /// ```
85    pub fn service_marker<M: ServiceMarker>(&self, _marker: M) -> ServiceConnector<'_, M::Proxy> {
86        ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData }
87    }
88
89    /// Creates a connector to the given service's default instance by its proxy type. This can be
90    /// convenient when the compiler can deduce the [`ServiceProxy`] type on its own.
91    ///
92    /// See [`ServiceConnector`] for more about what you can do with the connector.
93    ///
94    /// # Example
95    ///
96    /// ```ignore
97    /// struct MyProxies {
98    ///     i2c_service: fidl_fuchsia_hardware_i2c::ServiceProxy,
99    /// }
100    /// let proxies = MyProxies {
101    ///     i2c_service: context.incoming.service().connect()?;
102    /// };
103    /// ```
104    pub fn service<P>(&self) -> ServiceConnector<'_, P> {
105        ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData }
106    }
107}
108
109impl From<Vec<cm_types::NamespaceEntry>> for Incoming {
110    fn from(other: Vec<cm_types::NamespaceEntry>) -> Self {
111        Self(other.into_iter().map(Into::into).collect())
112    }
113}
114
115/// A builder for connecting to an aggregated service instance in the driver's incoming namespace.
116/// By default, it will connect to the default instance, named `default`. You can override this
117/// by calling [`Self::instance`].
118pub struct ServiceConnector<'incoming, ServiceProxy> {
119    incoming: &'incoming Incoming,
120    instance: &'incoming str,
121    _p: PhantomData<ServiceProxy>,
122}
123
124impl<'a, S> ServiceConnector<'a, S> {
125    /// Overrides the instance name to connect to when [`Self::connect`] is called.
126    pub fn instance(self, instance: &'a str) -> Self {
127        let Self { incoming, _p, .. } = self;
128        Self { incoming, instance, _p }
129    }
130}
131
132impl<'a, S: ServiceProxy> ServiceConnector<'a, S>
133where
134    S::Service: ServiceMarker,
135{
136    /// Connects to the service instance's path in the incoming namespace. Logs and returns
137    /// a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
138    pub fn connect(self) -> Result<S, Status> {
139        connect_to_service_instance_at_dir_svc::<S::Service>(self.incoming, self.instance).map_err(
140            |e| {
141                error!(
142                    "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}",
143                    S::Service::SERVICE_NAME,
144                    self.instance
145                );
146                Status::CONNECTION_REFUSED
147            },
148        )
149    }
150}
151
152/// Used with [`ServiceHandlerAdapter`] as a connector to members of a service instance.
153pub struct ServiceMemberConnector(fio::DirectoryProxy);
154
155fn connect(
156    dir: &fio::DirectoryProxy,
157    member: &str,
158    server_end: zx::Channel,
159) -> Result<(), fidl::Error> {
160    #[cfg(fuchsia_api_level_at_least = "27")]
161    return dir.open(member, fio::Flags::PROTOCOL_SERVICE, &fio::Options::default(), server_end);
162    #[cfg(not(fuchsia_api_level_at_least = "27"))]
163    return dir.open3(member, fio::Flags::PROTOCOL_SERVICE, &fio::Options::default(), server_end);
164}
165
166impl fidl_next_protocol::ServiceConnector<zx::Channel> for ServiceMemberConnector {
167    type Error = fidl::Error;
168    fn connect_to_member(&self, member: &str, server_end: zx::Channel) -> Result<(), Self::Error> {
169        connect(&self.0, member, server_end)
170    }
171}
172
173impl fidl_next_protocol::ServiceConnector<fdf_fidl::DriverChannel> for ServiceMemberConnector {
174    type Error = Status;
175    fn connect_to_member(
176        &self,
177        member: &str,
178        server_end: fdf_fidl::DriverChannel,
179    ) -> Result<(), Self::Error> {
180        let (client_token, server_token) = zx::Channel::create();
181
182        // SAFETY: client_token and server_end are valid by construction and `fdf_token_transfer`
183        // consumes both handles and does not interact with rust memory.
184        Status::ok(unsafe {
185            fdf_token_transfer(
186                client_token.into_raw(),
187                server_end.into_driver_handle().into_raw().get(),
188            )
189        })?;
190
191        connect(&self.0, member, server_token).map_err(|err| {
192            error!("Failed to connect to service member {member}: {err:?}");
193            Status::CONNECTION_REFUSED
194        })
195    }
196}
197
198/// A type alias representing a service instance with members that can be connected to using the
199/// [`fidl_next`] bindings.
200pub type ServiceInstance<S> = fidl_next_bind::ServiceConnector<S, ServiceMemberConnector>;
201
202impl<'a, S: Service<ServiceMemberConnector>> ServiceConnector<'a, ServiceInstance<S>> {
203    /// Connects to the service instance's path in the incoming namespace with the new wire bindings.
204    /// Logs and returns a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
205    pub fn connect_next(self) -> Result<ServiceInstance<S>, Status> {
206        let service_path = format!("{SVC_DIR}/{}/{}", S::SERVICE_NAME, self.instance);
207        let dir = open_directory_async(self.incoming, &service_path, fio::Rights::empty())
208            .map_err(|e| {
209                error!(
210                    "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}",
211                    S::SERVICE_NAME,
212                    self.instance
213                );
214                Status::CONNECTION_REFUSED
215            })?;
216        Ok(fidl_next_bind::ServiceConnector::from_untyped(ServiceMemberConnector(dir)))
217    }
218}
219
220impl From<Namespace> for Incoming {
221    fn from(value: Namespace) -> Self {
222        Incoming(value.flatten())
223    }
224}
225
226impl From<ClientEnd<fio::DirectoryMarker>> for Incoming {
227    fn from(client: ClientEnd<fio::DirectoryMarker>) -> Self {
228        Incoming(vec![Entry {
229            path: cm_types::NamespacePath::new("/").unwrap(),
230            directory: client,
231        }])
232    }
233}
234
235/// Returns the remainder of a prefix match of `prefix` against `self` in terms of path segments.
236///
237/// For example:
238/// ```ignore
239/// match_prefix("pkg/data", "pkg") == Some("/data")
240/// match_prefix("pkg_data", "pkg") == None
241/// ```
242fn match_prefix(match_in: &impl IterablePath, prefix: &impl IterablePath) -> Option<RelativePath> {
243    let mut my_segments = match_in.iter_segments();
244    let mut prefix_segments = prefix.iter_segments();
245    for prefix in prefix_segments.by_ref() {
246        if prefix != my_segments.next()? {
247            return None;
248        }
249    }
250    if prefix_segments.next().is_some() {
251        // did not match all prefix segments
252        return None;
253    }
254    let segments = Vec::from_iter(my_segments);
255    Some(RelativePath::from(segments))
256}
257
258impl Directory for Incoming {
259    fn open(&self, path: &str, flags: Flags, server_end: zx::Channel) -> Result<(), Error> {
260        let path = path.strip_prefix("/").unwrap_or(path);
261        let path = RelativePath::new(path)?;
262
263        for entry in &self.0 {
264            if let Some(remain) = match_prefix(&path, &entry.path) {
265                return entry.directory.open(&format!("{}", remain), flags, server_end);
266            }
267        }
268        Err(Status::NOT_FOUND)
269            .with_context(|| anyhow!("Path {path} not found in incoming namespace"))
270    }
271}
272
273impl AsRefDirectory for Incoming {
274    fn as_ref_directory(&self) -> &dyn Directory {
275        self
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use fuchsia_async::Task;
283    use fuchsia_component::server::ServiceFs;
284    use futures::stream::StreamExt;
285
286    enum IncomingServices {
287        Device(fidl_fuchsia_hardware_i2c::DeviceRequestStream),
288        DefaultService(fidl_fuchsia_hardware_i2c::ServiceRequest),
289        OtherService(fidl_fuchsia_hardware_i2c::ServiceRequest),
290    }
291
292    impl IncomingServices {
293        async fn handle_device_stream(
294            stream: fidl_fuchsia_hardware_i2c::DeviceRequestStream,
295            name: &str,
296        ) {
297            stream
298                .for_each(|msg| async move {
299                    match msg.unwrap() {
300                        fidl_fuchsia_hardware_i2c::DeviceRequest::GetName { responder } => {
301                            responder.send(Ok(name)).unwrap();
302                        }
303                        _ => unimplemented!(),
304                    }
305                })
306                .await
307        }
308
309        async fn handle(self) {
310            use IncomingServices::*;
311            match self {
312                Device(stream) => Self::handle_device_stream(stream, "device").await,
313                DefaultService(fidl_fuchsia_hardware_i2c::ServiceRequest::Device(stream)) => {
314                    Self::handle_device_stream(stream, "default").await
315                }
316                OtherService(fidl_fuchsia_hardware_i2c::ServiceRequest::Device(stream)) => {
317                    Self::handle_device_stream(stream, "other").await
318                }
319            }
320        }
321    }
322
323    async fn make_incoming() -> Incoming {
324        let (client, server) = fidl::endpoints::create_endpoints();
325        let mut fs = ServiceFs::new();
326        fs.dir("svc")
327            .add_fidl_service(IncomingServices::Device)
328            .add_fidl_service_instance("default", IncomingServices::DefaultService)
329            .add_fidl_service_instance("other", IncomingServices::OtherService);
330        fs.serve_connection(server).expect("error serving handle");
331
332        Task::spawn(fs.for_each_concurrent(100, IncomingServices::handle)).detach_on_drop();
333        Incoming::from(client)
334    }
335
336    #[fuchsia::test]
337    async fn protocol_connect_present() -> anyhow::Result<()> {
338        let incoming = make_incoming().await;
339        // try a protocol that we did set up
340        incoming
341            .connect_protocol::<fidl_fuchsia_hardware_i2c::DeviceProxy>()?
342            .get_name()
343            .await?
344            .unwrap();
345        Ok(())
346    }
347
348    #[fuchsia::test]
349    async fn protocol_connect_not_present() -> anyhow::Result<()> {
350        let incoming = make_incoming().await;
351        // try one we didn't
352        incoming
353            .connect_protocol::<fidl_fuchsia_hwinfo::DeviceProxy>()?
354            .get_info()
355            .await
356            .unwrap_err();
357        Ok(())
358    }
359
360    #[fuchsia::test]
361    async fn service_connect_default_instance() -> anyhow::Result<()> {
362        let incoming = make_incoming().await;
363        // try the default service instance that we did set up
364        assert_eq!(
365            "default",
366            &incoming
367                .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
368                .connect()?
369                .connect_to_device()?
370                .get_name()
371                .await?
372                .unwrap()
373        );
374        assert_eq!(
375            "default",
376            &incoming
377                .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
378                .connect()?
379                .connect_to_device()?
380                .get_name()
381                .await?
382                .unwrap()
383        );
384        Ok(())
385    }
386
387    #[fuchsia::test]
388    async fn service_connect_other_instance() -> anyhow::Result<()> {
389        let incoming = make_incoming().await;
390        // try the other service instance that we did set up
391        assert_eq!(
392            "other",
393            &incoming
394                .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
395                .instance("other")
396                .connect()?
397                .connect_to_device()?
398                .get_name()
399                .await?
400                .unwrap()
401        );
402        assert_eq!(
403            "other",
404            &incoming
405                .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
406                .instance("other")
407                .connect()?
408                .connect_to_device()?
409                .get_name()
410                .await?
411                .unwrap()
412        );
413        Ok(())
414    }
415
416    #[fuchsia::test]
417    async fn service_connect_invalid_instance() -> anyhow::Result<()> {
418        let incoming = make_incoming().await;
419        // try the invalid service instance that we did not set up
420        incoming
421            .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
422            .instance("invalid")
423            .connect()?
424            .connect_to_device()?
425            .get_name()
426            .await
427            .unwrap_err();
428        incoming
429            .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
430            .instance("invalid")
431            .connect()?
432            .connect_to_device()?
433            .get_name()
434            .await
435            .unwrap_err();
436        Ok(())
437    }
438}