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::{anyhow, Context, Error};
8use cm_types::{IterablePath, RelativePath};
9use fidl::endpoints::{DiscoverableProtocolMarker, ServiceMarker, ServiceProxy};
10use fidl_fuchsia_io::Flags;
11use fuchsia_component::client::{connect_to_service_instance_at_dir_svc, Connect};
12use fuchsia_component::directory::{AsRefDirectory, Directory};
13use fuchsia_component::DEFAULT_SERVICE_INSTANCE;
14use log::error;
15use namespace::{Entry, Namespace};
16use zx::Status;
17
18/// Implements access to the incoming namespace for a driver. It provides methods
19/// for accessing incoming protocols and services by either their marker or proxy
20/// types, and can be used as a [`Directory`] with the functions in
21/// [`fuchsia_component::client`].
22pub struct Incoming(Vec<Entry>);
23
24impl Incoming {
25    /// Connects to the protocol in the service instance's path in the incoming namespace. Logs and
26    /// returns a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
27    pub fn connect_protocol<T: Connect>(&self) -> Result<T, Status> {
28        T::connect_at_dir_svc(&self).map_err(|e| {
29            error!(
30                "Failed to connect to discoverable protocol `{}`: {e}",
31                T::Protocol::PROTOCOL_NAME
32            );
33            Status::CONNECTION_REFUSED
34        })
35    }
36
37    /// Creates a connector to the given service's default instance by its marker type. This can be
38    /// convenient when the compiler can't deduce the [`ServiceProxy`] type on its own.
39    ///
40    /// See [`ServiceConnector`] for more about what you can do with the connector.
41    ///
42    /// # Example
43    ///
44    /// ```ignore
45    /// let service = context.incoming.service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker).connect()?;
46    /// let device = service.connect_to_device()?;
47    /// ```
48    pub fn service_marker<M: ServiceMarker>(&self, _marker: M) -> ServiceConnector<'_, M::Proxy> {
49        ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData }
50    }
51
52    /// Creates a connector to the given service's default instance by its proxy type. This can be
53    /// convenient when the compiler can deduce the [`ServiceProxy`] type on its own.
54    ///
55    /// See [`ServiceConnector`] for more about what you can do with the connector.
56    ///
57    /// # Example
58    ///
59    /// ```ignore
60    /// struct MyProxies {
61    ///     i2c_service: fidl_fuchsia_hardware_i2c::ServiceProxy,
62    /// }
63    /// let proxies = MyProxies {
64    ///     i2c_service: context.incoming.service().connect()?;
65    /// };
66    /// ```
67    pub fn service<P>(&self) -> ServiceConnector<'_, P> {
68        ServiceConnector { incoming: self, instance: DEFAULT_SERVICE_INSTANCE, _p: PhantomData }
69    }
70}
71
72/// A builder for connecting to an aggregated service instance in the driver's incoming namespace.
73/// By default, it will connect to the default instance, named `default`. You can override this
74/// by calling [`Self::instance`].
75pub struct ServiceConnector<'incoming, ServiceProxy> {
76    incoming: &'incoming Incoming,
77    instance: &'incoming str,
78    _p: PhantomData<ServiceProxy>,
79}
80
81impl<'a, S: ServiceProxy> ServiceConnector<'a, S>
82where
83    S::Service: ServiceMarker,
84{
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    /// Connects to the service instance's path in the incoming namespace. Logs and returns
92    /// a [`Status::CONNECTION_REFUSED`] if the service instance couldn't be opened.
93    pub fn connect(self) -> Result<S, Status> {
94        connect_to_service_instance_at_dir_svc::<S::Service>(self.incoming, self.instance).map_err(
95            |e| {
96                error!(
97                    "Failed to connect to aggregated service connector `{}`, instance `{}`: {e}",
98                    S::Service::SERVICE_NAME,
99                    self.instance
100                );
101                Status::CONNECTION_REFUSED
102            },
103        )
104    }
105}
106
107impl From<Namespace> for Incoming {
108    fn from(value: Namespace) -> Self {
109        Incoming(value.flatten())
110    }
111}
112
113/// Returns the remainder of a prefix match of `prefix` against `self` in terms of path segments.
114///
115/// For example:
116/// ```ignore
117/// match_prefix("pkg/data", "pkg") == Some("/data")
118/// match_prefix("pkg_data", "pkg") == None
119/// ```
120fn match_prefix(match_in: &impl IterablePath, prefix: &impl IterablePath) -> Option<RelativePath> {
121    let mut my_segments = match_in.iter_segments();
122    let mut prefix_segments = prefix.iter_segments();
123    while let Some(prefix) = prefix_segments.next() {
124        if prefix != my_segments.next()? {
125            return None;
126        }
127    }
128    if prefix_segments.next().is_some() {
129        // did not match all prefix segments
130        return None;
131    }
132    let segments = Vec::from_iter(my_segments);
133    Some(RelativePath::from(segments))
134}
135
136impl Directory for Incoming {
137    fn open(&self, path: &str, flags: Flags, server_end: zx::Channel) -> Result<(), Error> {
138        let path = path.strip_prefix("/").unwrap_or(path);
139        let path = RelativePath::new(path)?;
140
141        for entry in &self.0 {
142            if let Some(remain) = match_prefix(&path, &entry.path) {
143                return entry.directory.open(&format!("/{}", remain), flags, server_end);
144            }
145        }
146        Err(Status::NOT_FOUND)
147            .with_context(|| anyhow!("Path {path} not found in incoming namespace"))
148    }
149}
150
151impl AsRefDirectory for Incoming {
152    fn as_ref_directory(&self) -> &dyn Directory {
153        self
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use cm_types::NamespacePath;
161    use fuchsia_async::Task;
162    use fuchsia_component::server::ServiceFs;
163    use futures::stream::StreamExt;
164
165    enum IncomingServices {
166        I2cDevice(fidl_fuchsia_hardware_i2c::DeviceRequestStream),
167        I2cDefaultService(fidl_fuchsia_hardware_i2c::ServiceRequest),
168        I2cOtherService(fidl_fuchsia_hardware_i2c::ServiceRequest),
169    }
170
171    impl IncomingServices {
172        async fn handle_device_stream(
173            stream: fidl_fuchsia_hardware_i2c::DeviceRequestStream,
174            name: &str,
175        ) {
176            stream
177                .for_each(|msg| async move {
178                    match msg.unwrap() {
179                        fidl_fuchsia_hardware_i2c::DeviceRequest::GetName { responder } => {
180                            responder.send(Ok(name)).unwrap();
181                        }
182                        _ => unimplemented!(),
183                    }
184                })
185                .await
186        }
187
188        async fn handle(self) {
189            use fidl_fuchsia_hardware_i2c::ServiceRequest::*;
190            use IncomingServices::*;
191            match self {
192                I2cDevice(stream) => Self::handle_device_stream(stream, "device").await,
193                I2cDefaultService(Device(stream)) => {
194                    Self::handle_device_stream(stream, "default").await
195                }
196                I2cOtherService(Device(stream)) => {
197                    Self::handle_device_stream(stream, "other").await
198                }
199            }
200        }
201    }
202
203    async fn make_incoming() -> Incoming {
204        let (client, server) = fidl::endpoints::create_endpoints();
205        let mut fs = ServiceFs::new();
206        fs.dir("svc")
207            .add_fidl_service(IncomingServices::I2cDevice)
208            .add_fidl_service_instance("default", IncomingServices::I2cDefaultService)
209            .add_fidl_service_instance("other", IncomingServices::I2cOtherService);
210        fs.serve_connection(server).expect("error serving handle");
211
212        Task::spawn(fs.for_each_concurrent(100, IncomingServices::handle)).detach_on_drop();
213
214        Incoming(vec![Entry { path: NamespacePath::new("/").unwrap(), directory: client }])
215    }
216
217    #[fuchsia::test]
218    async fn protocol_connect_present() -> anyhow::Result<()> {
219        let incoming = make_incoming().await;
220        // try a protocol that we did set up
221        incoming
222            .connect_protocol::<fidl_fuchsia_hardware_i2c::DeviceProxy>()?
223            .get_name()
224            .await?
225            .unwrap();
226        Ok(())
227    }
228
229    #[fuchsia::test]
230    async fn protocol_connect_not_present() -> anyhow::Result<()> {
231        let incoming = make_incoming().await;
232        // try one we didn't
233        incoming
234            .connect_protocol::<fidl_fuchsia_hwinfo::DeviceProxy>()?
235            .get_info()
236            .await
237            .unwrap_err();
238        Ok(())
239    }
240
241    #[fuchsia::test]
242    async fn service_connect_default_instance() -> anyhow::Result<()> {
243        let incoming = make_incoming().await;
244        // try the default service instance that we did set up
245        assert_eq!(
246            "default",
247            &incoming
248                .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
249                .connect()?
250                .connect_to_device()?
251                .get_name()
252                .await?
253                .unwrap()
254        );
255        assert_eq!(
256            "default",
257            &incoming
258                .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
259                .connect()?
260                .connect_to_device()?
261                .get_name()
262                .await?
263                .unwrap()
264        );
265        Ok(())
266    }
267
268    #[fuchsia::test]
269    async fn service_connect_other_instance() -> anyhow::Result<()> {
270        let incoming = make_incoming().await;
271        // try the other service instance that we did set up
272        assert_eq!(
273            "other",
274            &incoming
275                .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
276                .instance("other")
277                .connect()?
278                .connect_to_device()?
279                .get_name()
280                .await?
281                .unwrap()
282        );
283        assert_eq!(
284            "other",
285            &incoming
286                .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
287                .instance("other")
288                .connect()?
289                .connect_to_device()?
290                .get_name()
291                .await?
292                .unwrap()
293        );
294        Ok(())
295    }
296
297    #[fuchsia::test]
298    async fn service_connect_invalid_instance() -> anyhow::Result<()> {
299        let incoming = make_incoming().await;
300        // try the invalid service instance that we did not set up
301        incoming
302            .service_marker(fidl_fuchsia_hardware_i2c::ServiceMarker)
303            .instance("invalid")
304            .connect()?
305            .connect_to_device()?
306            .get_name()
307            .await
308            .unwrap_err();
309        incoming
310            .service::<fidl_fuchsia_hardware_i2c::ServiceProxy>()
311            .instance("invalid")
312            .connect()?
313            .connect_to_device()?
314            .get_name()
315            .await
316            .unwrap_err();
317        Ok(())
318    }
319}