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