fuchsia_component_client/
lib.rs

1// Copyright 2019 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
5//! Tools for starting or connecting to existing Fuchsia applications and services.
6
7#![deny(missing_docs)]
8
9pub mod fidl_next;
10
11use anyhow::{Context as _, Error, format_err};
12use fidl::endpoints::{
13    DiscoverableProtocolMarker, FromClient, MemberOpener, ProtocolMarker, ServiceMarker,
14    ServiceProxy,
15};
16use fidl_fuchsia_component::{RealmMarker, RealmProxy};
17use fidl_fuchsia_component_decl::ChildRef;
18use fidl_fuchsia_io as fio;
19use fuchsia_component_directory::{AsRefDirectory, open_directory_async};
20use fuchsia_fs::directory::{WatchEvent, Watcher};
21use futures::stream::FusedStream;
22use futures::{Stream, StreamExt};
23use pin_project::pin_project;
24use std::borrow::Borrow;
25use std::marker::PhantomData;
26use std::pin::pin;
27use std::task::Poll;
28
29/// Trait that for types that can be returned when you connect to protocols.
30pub trait Connect: Sized + FromClient<Protocol: DiscoverableProtocolMarker> {
31    /// Connect to a FIDL protocol in the `/svc` directory of the application's root namespace.
32    fn connect() -> Result<Self, Error> {
33        Self::connect_at(SVC_DIR)
34    }
35
36    /// Connect to a FIDL protocol using the provided namespace prefix.
37    fn connect_at(service_prefix: impl AsRef<str>) -> Result<Self, Error> {
38        let (client, server_end) = fidl::endpoints::create_endpoints::<Self::Protocol>();
39        let () = connect_channel_to_protocol_at::<Self::Protocol>(
40            server_end.into_channel(),
41            service_prefix.as_ref(),
42        )?;
43        Ok(Self::from_client(client))
44    }
45
46    /// Connect to an instance of a FIDL protocol hosted in `directory`, in the `/svc/` subdir.
47    fn connect_at_dir_svc(directory: &impl AsRefDirectory) -> Result<Self, Error> {
48        let protocol_path = format!("{}/{}", SVC_DIR, Self::Protocol::PROTOCOL_NAME);
49        Self::connect_at_dir_root_with_name(directory, &protocol_path)
50    }
51
52    /// Connect to an instance of a FIDL protocol hosted in `directory`.
53    fn connect_at_dir_root(directory: &impl AsRefDirectory) -> Result<Self, Error> {
54        Self::connect_at_dir_root_with_name(directory, Self::Protocol::PROTOCOL_NAME)
55    }
56
57    /// Connect to an instance of a FIDL protocol hosted in `directory` using the given `filename`.
58    fn connect_at_dir_root_with_name(
59        directory: &impl AsRefDirectory,
60        filename: &str,
61    ) -> Result<Self, Error> {
62        let (client, server) = fidl::endpoints::create_endpoints::<Self::Protocol>();
63        directory.as_ref_directory().open(
64            filename,
65            fio::Flags::PROTOCOL_SERVICE,
66            server.into_channel().into(),
67        )?;
68        Ok(Self::from_client(client))
69    }
70}
71
72impl<T: FromClient<Protocol: DiscoverableProtocolMarker>> Connect for T {}
73
74/// The connect module provides connect methods where the generic argument is the return type rather
75/// than the marker type. This allows them to work for different return types; the same method can
76/// be used for asynchronous proxies, synchronous proxies, client ends, etc.  This also makes it
77/// possible to elide the type where the compiler can infer the type, which is not the case when
78/// using the marker types.
79pub mod connect {
80    use super::*;
81
82    /// Connect to a FIDL protocol in the `/svc` directory of the application's root namespace.
83    pub fn connect_to_protocol<T: Connect>() -> Result<T, Error> {
84        T::connect()
85    }
86
87    /// Connect to a FIDL protocol using the provided namespace prefix.
88    pub fn connect_to_protocol_at<T: Connect>(service_prefix: impl AsRef<str>) -> Result<T, Error> {
89        T::connect_at(service_prefix)
90    }
91
92    /// Connect to an instance of a FIDL protocol hosted in `directory`, in the `/svc/` subdir.
93    pub fn connect_to_protocol_at_dir_svc<T: Connect>(
94        directory: &impl AsRefDirectory,
95    ) -> Result<T, Error> {
96        T::connect_at_dir_svc(directory)
97    }
98
99    /// Connect to an instance of a FIDL protocol hosted in `directory`.
100    pub fn connect_to_protocol_at_dir_root<T: Connect>(
101        directory: &impl AsRefDirectory,
102    ) -> Result<T, Error> {
103        T::connect_at_dir_root(directory)
104    }
105
106    /// Connect to an instance of a FIDL protocol hosted in `directory` using the given `filename`.
107    pub fn connect_to_named_protocol_at_dir_root<T: Connect>(
108        directory: &impl AsRefDirectory,
109        filename: &str,
110    ) -> Result<T, Error> {
111        T::connect_at_dir_root_with_name(directory, filename)
112    }
113}
114
115/// Path to the service directory in an application's root namespace.
116pub const SVC_DIR: &'static str = "/svc";
117
118/// A protocol connection request that allows checking if the protocol exists.
119pub struct ProtocolConnector<D: Borrow<fio::DirectoryProxy>, P: DiscoverableProtocolMarker> {
120    svc_dir: D,
121    _svc_marker: PhantomData<P>,
122}
123
124impl<D: Borrow<fio::DirectoryProxy>, P: DiscoverableProtocolMarker> ProtocolConnector<D, P> {
125    /// Returns a new `ProtocolConnector` to `P` in the specified service directory.
126    fn new(svc_dir: D) -> ProtocolConnector<D, P> {
127        ProtocolConnector { svc_dir, _svc_marker: PhantomData }
128    }
129
130    /// Returns `true` if the protocol exists in the service directory.
131    ///
132    /// This method requires a round trip to the service directory to check for
133    /// existence.
134    pub async fn exists(&self) -> Result<bool, Error> {
135        match fuchsia_fs::directory::dir_contains(self.svc_dir.borrow(), P::PROTOCOL_NAME).await {
136            Ok(v) => Ok(v),
137            // If the service directory is unavailable, then mask the error as if
138            // the protocol does not exist.
139            Err(fuchsia_fs::directory::EnumerateError::Fidl(
140                _,
141                fidl::Error::ClientChannelClosed { status, .. },
142            )) if status == zx::Status::PEER_CLOSED => Ok(false),
143            Err(e) => Err(Error::new(e).context("error checking for service entry in directory")),
144        }
145    }
146
147    /// Connect to the FIDL protocol using the provided server-end.
148    ///
149    /// Note, this method does not check if the protocol exists. It is up to the
150    /// caller to call `exists` to check for existence.
151    pub fn connect_with(self, server_end: zx::Channel) -> Result<(), Error> {
152        #[cfg(fuchsia_api_level_at_least = "27")]
153        return self
154            .svc_dir
155            .borrow()
156            .open(
157                P::PROTOCOL_NAME,
158                fio::Flags::PROTOCOL_SERVICE,
159                &fio::Options::default(),
160                server_end.into(),
161            )
162            .context("error connecting to protocol");
163        #[cfg(not(fuchsia_api_level_at_least = "27"))]
164        return self
165            .svc_dir
166            .borrow()
167            .open3(
168                P::PROTOCOL_NAME,
169                fio::Flags::PROTOCOL_SERVICE,
170                &fio::Options::default(),
171                server_end.into(),
172            )
173            .context("error connecting to protocol");
174    }
175
176    /// Connect to the FIDL protocol.
177    ///
178    /// Note, this method does not check if the protocol exists. It is up to the
179    /// caller to call `exists` to check for existence.
180    pub fn connect(self) -> Result<P::Proxy, Error> {
181        let (proxy, server_end) = fidl::endpoints::create_proxy::<P>();
182        let () = self
183            .connect_with(server_end.into_channel())
184            .context("error connecting with server channel")?;
185        Ok(proxy)
186    }
187}
188
189/// Clone the handle to the service directory in the application's root namespace.
190pub fn clone_namespace_svc() -> Result<fio::DirectoryProxy, Error> {
191    fuchsia_fs::directory::open_in_namespace(SVC_DIR, fio::Flags::empty())
192        .context("error opening svc directory")
193}
194
195/// Return a FIDL protocol connector at the default service directory in the
196/// application's root namespace.
197pub fn new_protocol_connector<P: DiscoverableProtocolMarker>()
198-> Result<ProtocolConnector<fio::DirectoryProxy, P>, Error> {
199    new_protocol_connector_at::<P>(SVC_DIR)
200}
201
202/// Return a FIDL protocol connector at the specified service directory in the
203/// application's root namespace.
204///
205/// The service directory path must be an absolute path.
206pub fn new_protocol_connector_at<P: DiscoverableProtocolMarker>(
207    service_directory_path: &str,
208) -> Result<ProtocolConnector<fio::DirectoryProxy, P>, Error> {
209    let dir = fuchsia_fs::directory::open_in_namespace(service_directory_path, fio::Flags::empty())
210        .context("error opening service directory")?;
211
212    Ok(ProtocolConnector::new(dir))
213}
214
215/// Return a FIDL protocol connector at the specified service directory.
216pub fn new_protocol_connector_in_dir<P: DiscoverableProtocolMarker>(
217    dir: &fio::DirectoryProxy,
218) -> ProtocolConnector<&fio::DirectoryProxy, P> {
219    ProtocolConnector::new(dir)
220}
221
222/// Connect to a FIDL protocol using the provided channel.
223pub fn connect_channel_to_protocol<P: DiscoverableProtocolMarker>(
224    server_end: zx::Channel,
225) -> Result<(), Error> {
226    connect_channel_to_protocol_at::<P>(server_end, SVC_DIR)
227}
228
229/// Connect to a FIDL protocol using the provided channel and namespace prefix.
230pub fn connect_channel_to_protocol_at<P: DiscoverableProtocolMarker>(
231    server_end: zx::Channel,
232    service_directory_path: &str,
233) -> Result<(), Error> {
234    let protocol_path = format!("{}/{}", service_directory_path, P::PROTOCOL_NAME);
235    connect_channel_to_protocol_at_path(server_end, &protocol_path)
236}
237
238/// Connect to a FIDL protocol using the provided channel and namespace path.
239pub fn connect_channel_to_protocol_at_path(
240    server_end: zx::Channel,
241    protocol_path: &str,
242) -> Result<(), Error> {
243    fdio::service_connect(&protocol_path, server_end)
244        .with_context(|| format!("Error connecting to protocol path: {}", protocol_path))
245}
246
247/// Connect to a FIDL protocol in the `/svc` directory of the application's root namespace.
248pub fn connect_to_protocol<P: DiscoverableProtocolMarker>() -> Result<P::Proxy, Error> {
249    connect_to_protocol_at::<P>(SVC_DIR)
250}
251
252/// Connect to a FIDL protocol using the application root namespace, returning a synchronous proxy.
253///
254/// Note: while this function returns a synchronous thread-blocking proxy it does not block until
255/// the connection is complete. The proxy must be used to discover whether the connection was
256/// successful.
257pub fn connect_to_protocol_sync<P: DiscoverableProtocolMarker>()
258-> Result<P::SynchronousProxy, Error> {
259    connect_to_protocol_sync_at::<P>(SVC_DIR)
260}
261
262/// Connect to a FIDL protocol using the provided namespace prefix.
263pub fn connect_to_protocol_at<P: DiscoverableProtocolMarker>(
264    service_prefix: impl AsRef<str>,
265) -> Result<P::Proxy, Error> {
266    let (proxy, server_end) = fidl::endpoints::create_proxy::<P>();
267    let () =
268        connect_channel_to_protocol_at::<P>(server_end.into_channel(), service_prefix.as_ref())?;
269    Ok(proxy)
270}
271
272/// Connect to a FIDL protocol using the provided namespace prefix, returning a synchronous proxy.
273///
274/// Note: while this function returns a synchronous thread-blocking proxy it does not block until
275/// the connection is complete. The proxy must be used to discover whether the connection was
276/// successful.
277pub fn connect_to_protocol_sync_at<P: DiscoverableProtocolMarker>(
278    service_prefix: impl AsRef<str>,
279) -> Result<P::SynchronousProxy, Error> {
280    let (proxy, server_end) = fidl::endpoints::create_sync_proxy::<P>();
281    let () =
282        connect_channel_to_protocol_at::<P>(server_end.into_channel(), service_prefix.as_ref())?;
283    Ok(proxy)
284}
285
286/// Connect to a FIDL protocol using the provided path.
287pub fn connect_to_protocol_at_path<P: ProtocolMarker>(
288    protocol_path: impl AsRef<str>,
289) -> Result<P::Proxy, Error> {
290    let (proxy, server_end) = fidl::endpoints::create_proxy::<P>();
291    let () =
292        connect_channel_to_protocol_at_path(server_end.into_channel(), protocol_path.as_ref())?;
293    Ok(proxy)
294}
295
296/// Connect to an instance of a FIDL protocol hosted in `directory`.
297pub fn connect_to_protocol_at_dir_root<P: DiscoverableProtocolMarker>(
298    directory: &impl AsRefDirectory,
299) -> Result<P::Proxy, Error> {
300    connect_to_named_protocol_at_dir_root::<P>(directory, P::PROTOCOL_NAME)
301}
302
303/// Connect to an instance of a FIDL protocol hosted in `directory` using the given `filename`.
304pub fn connect_to_named_protocol_at_dir_root<P: ProtocolMarker>(
305    directory: &impl AsRefDirectory,
306    filename: &str,
307) -> Result<P::Proxy, Error> {
308    let (proxy, server_end) = fidl::endpoints::create_proxy::<P>();
309    directory.as_ref_directory().open(
310        filename,
311        fio::Flags::PROTOCOL_SERVICE,
312        server_end.into_channel().into(),
313    )?;
314    Ok(proxy)
315}
316
317/// Connect to an instance of a FIDL protocol hosted in `directory`, in the `/svc/` subdir.
318pub fn connect_to_protocol_at_dir_svc<P: DiscoverableProtocolMarker>(
319    directory: &impl AsRefDirectory,
320) -> Result<P::Proxy, Error> {
321    let protocol_path = format!("{}/{}", SVC_DIR, P::PROTOCOL_NAME);
322    connect_to_named_protocol_at_dir_root::<P>(directory, &protocol_path)
323}
324
325/// This wraps an instance directory for a service capability and provides the MemberOpener trait
326/// for it. This can be boxed and used with a |ServiceProxy::from_member_opener|.
327pub struct ServiceInstanceDirectory(pub fio::DirectoryProxy, pub String);
328
329impl MemberOpener for ServiceInstanceDirectory {
330    fn open_member(&self, member: &str, server_end: zx::Channel) -> Result<(), fidl::Error> {
331        let Self(directory, _) = self;
332        #[cfg(fuchsia_api_level_at_least = "27")]
333        return directory.open(
334            member,
335            fio::Flags::PROTOCOL_SERVICE,
336            &fio::Options::default(),
337            server_end,
338        );
339        #[cfg(not(fuchsia_api_level_at_least = "27"))]
340        return directory.open3(
341            member,
342            fio::Flags::PROTOCOL_SERVICE,
343            &fio::Options::default(),
344            server_end,
345        );
346    }
347    fn instance_name(&self) -> &str {
348        let Self(_, instance_name) = self;
349        return &instance_name;
350    }
351}
352
353/// An instance of an aggregated fidl service that has been enumerated by [`ServiceWatcher::watch`]
354/// or [`ServiceWatcher::watch_for_any`].
355struct ServiceInstance<S> {
356    /// The name of the service instance within the service directory
357    name: String,
358    service: Service<S>,
359}
360
361impl<S: ServiceMarker> MemberOpener for ServiceInstance<S> {
362    fn open_member(&self, member: &str, server_end: zx::Channel) -> Result<(), fidl::Error> {
363        self.service.connect_to_instance_member_with_channel(&self.name, member, server_end)
364    }
365    fn instance_name(&self) -> &str {
366        return &self.name;
367    }
368}
369
370/// A service from an incoming namespace's `/svc` directory.
371pub struct Service<S> {
372    dir: fio::DirectoryProxy,
373    _marker: S,
374}
375
376impl<S: Clone> Clone for Service<S> {
377    fn clone(&self) -> Self {
378        Self { dir: Clone::clone(&self.dir), _marker: self._marker.clone() }
379    }
380}
381
382/// Returns a new [`Service`] that waits for instances to appear in
383/// the given service directory, probably opened by [`open_service`]
384impl<S> From<fio::DirectoryProxy> for Service<S>
385where
386    S: Default,
387{
388    fn from(dir: fio::DirectoryProxy) -> Self {
389        Self { dir, _marker: S::default() }
390    }
391}
392
393impl<S: ServiceMarker> Service<S> {
394    /// Returns a new [`Service`] that waits for instances to appear in
395    /// the given service directory, probably opened by [`open_service`]
396    pub fn from_service_dir_proxy(dir: fio::DirectoryProxy, _marker: S) -> Self {
397        Self { dir, _marker }
398    }
399
400    /// Returns a new [`Service`] from the process's incoming service namespace.
401    pub fn open(marker: S) -> Result<Self, Error> {
402        Ok(Self::from_service_dir_proxy(open_service::<S>()?, marker))
403    }
404
405    /// Returns a new [`Service`] from the process's incoming service namespace.
406    pub fn open_at(service_name: impl AsRef<str>, marker: S) -> Result<Self, Error> {
407        Ok(Self::from_service_dir_proxy(open_service_at(service_name)?, marker))
408    }
409
410    /// Returns a new [`Service`] that is in the given directory.
411    pub fn open_from_dir(svc_dir: impl AsRefDirectory, marker: S) -> Result<Self, Error> {
412        let dir = open_directory_async(&svc_dir, S::SERVICE_NAME, fio::Rights::empty())?;
413        Ok(Self::from_service_dir_proxy(dir, marker))
414    }
415
416    /// Returns a new [`Service`] that is in the given directory under the given prefix
417    /// (as "{prefix}/ServiceName"). A common case would be passing [`SVC_DIR`] as the prefix.
418    pub fn open_from_dir_prefix(
419        dir: impl AsRefDirectory,
420        prefix: impl AsRef<str>,
421        marker: S,
422    ) -> Result<Self, Error> {
423        let prefix = prefix.as_ref();
424        let service_path = format!("{prefix}/{}", S::SERVICE_NAME);
425        // TODO(https://fxbug.dev/42068248): Some Directory implementations require relative paths,
426        // even though they aren't technically supposed to, so strip the leading slash until that's
427        // resolved one way or the other.
428        let service_path = service_path.strip_prefix('/').unwrap_or_else(|| service_path.as_ref());
429        let dir = open_directory_async(&dir, &service_path, fio::Rights::empty())?;
430        Ok(Self::from_service_dir_proxy(dir, marker))
431    }
432
433    /// Connects to the named instance without waiting for it to appear. You should only use this
434    /// after the instance name has been returned by the [`Self::watch`] stream, or if the
435    /// instance is statically routed so component manager will lazily load it.
436    pub fn connect_to_instance(&self, name: impl AsRef<str>) -> Result<S::Proxy, Error> {
437        let directory_proxy = fuchsia_fs::directory::open_directory_async(
438            &self.dir,
439            name.as_ref(),
440            fio::Flags::empty(),
441        )?;
442        Ok(S::Proxy::from_member_opener(Box::new(ServiceInstanceDirectory(
443            directory_proxy,
444            name.as_ref().to_string(),
445        ))))
446    }
447
448    /// Connects to the named instance member without waiting for it to appear. You should only use this
449    /// after the instance name has been returned by the [`Self::watch`] stream, or if the
450    /// instance is statically routed so component manager will lazily load it.
451    fn connect_to_instance_member_with_channel(
452        &self,
453        instance: impl AsRef<str>,
454        member: impl AsRef<str>,
455        server_end: zx::Channel,
456    ) -> Result<(), fidl::Error> {
457        let path = format!("{}/{}", instance.as_ref(), member.as_ref());
458        #[cfg(fuchsia_api_level_at_least = "27")]
459        return self.dir.open(
460            &path,
461            fio::Flags::PROTOCOL_SERVICE,
462            &fio::Options::default(),
463            server_end,
464        );
465        #[cfg(not(fuchsia_api_level_at_least = "27"))]
466        return self.dir.open3(
467            &path,
468            fio::Flags::PROTOCOL_SERVICE,
469            &fio::Options::default(),
470            server_end,
471        );
472    }
473
474    /// Returns an async stream of service instances that are enumerated within this service
475    /// directory.
476    pub async fn watch(self) -> Result<ServiceInstanceStream<S>, Error> {
477        let watcher = Watcher::new(&self.dir).await?;
478        let finished = false;
479        Ok(ServiceInstanceStream { service: self, watcher, finished })
480    }
481
482    /// Asynchronously returns the first service instance available within this service directory.
483    pub async fn watch_for_any(self) -> Result<S::Proxy, Error> {
484        self.watch()
485            .await?
486            .next()
487            .await
488            .context("No instances found before service directory was removed")?
489    }
490
491    /// Returns an list of all instances that are currently available.
492    pub async fn enumerate(self) -> Result<Vec<S::Proxy>, Error> {
493        let instances: Vec<S::Proxy> = fuchsia_fs::directory::readdir(&self.dir)
494            .await?
495            .into_iter()
496            .map(|dirent| {
497                S::Proxy::from_member_opener(Box::new(ServiceInstance {
498                    service: self.clone(),
499                    name: dirent.name,
500                }))
501            })
502            .collect();
503        Ok(instances)
504    }
505}
506
507/// A stream iterator for a service directory that produces one item for every service instance
508/// that is added to it as they are added. Returned from [`Service::watch`]
509///
510/// Normally, this stream will only terminate if the service directory being watched is removed, so
511/// the client must decide when it has found all the instances it's looking for.
512#[pin_project]
513pub struct ServiceInstanceStream<S: Clone> {
514    service: Service<S>,
515    watcher: Watcher,
516    finished: bool,
517}
518
519impl<S: ServiceMarker> Stream for ServiceInstanceStream<S> {
520    type Item = Result<S::Proxy, Error>;
521
522    fn poll_next(
523        self: std::pin::Pin<&mut Self>,
524        cx: &mut std::task::Context<'_>,
525    ) -> Poll<Option<Self::Item>> {
526        let this = self.project();
527        use Poll::*;
528        if *this.finished {
529            return Poll::Ready(None);
530        }
531        // poll the inner watcher until we either find something worth returning or it
532        // returns Pending.
533        while let Ready(next) = this.watcher.poll_next_unpin(cx) {
534            match next {
535                Some(Ok(state)) => match state.event {
536                    WatchEvent::DELETED => {
537                        *this.finished = true;
538                        return Ready(None);
539                    }
540                    WatchEvent::ADD_FILE | WatchEvent::EXISTING => {
541                        let filename = state.filename.to_str().unwrap();
542                        if filename != "." {
543                            let proxy = S::Proxy::from_member_opener(Box::new(ServiceInstance {
544                                service: this.service.clone(),
545                                name: filename.to_owned(),
546                            }));
547                            return Ready(Some(Ok(proxy)));
548                        }
549                    }
550                    _ => {}
551                },
552                Some(Err(err)) => {
553                    *this.finished = true;
554                    return Ready(Some(Err(err.into())));
555                }
556                None => {
557                    *this.finished = true;
558                    return Ready(None);
559                }
560            }
561        }
562        Pending
563    }
564}
565
566impl<S: ServiceMarker> FusedStream for ServiceInstanceStream<S> {
567    fn is_terminated(&self) -> bool {
568        self.finished
569    }
570}
571
572/// Connect to an instance of a FIDL service in the `/svc` directory of
573/// the application's root namespace.
574/// `instance` is a path of one or more components.
575// NOTE: We would like to use impl AsRef<T> to accept a wide variety of string-like
576// inputs but Rust limits specifying explicit generic parameters when `impl-traits`
577// are present.
578pub fn connect_to_service_instance<S: ServiceMarker>(instance: &str) -> Result<S::Proxy, Error> {
579    let service_path = format!("{}/{}/{}", SVC_DIR, S::SERVICE_NAME, instance);
580    let directory_proxy =
581        fuchsia_fs::directory::open_in_namespace(&service_path, fio::Flags::empty())?;
582    Ok(S::Proxy::from_member_opener(Box::new(ServiceInstanceDirectory(
583        directory_proxy,
584        instance.to_string(),
585    ))))
586}
587
588/// Connect to a named instance of a FIDL service hosted in the service subdirectory under the
589/// directory protocol channel `directory`
590// NOTE: We would like to use impl AsRef<T> to accept a wide variety of string-like
591// inputs but Rust limits specifying explicit generic parameters when `impl-traits`
592// are present.
593pub fn connect_to_service_instance_at_dir<S: ServiceMarker>(
594    directory: &fio::DirectoryProxy,
595    instance: &str,
596) -> Result<S::Proxy, Error> {
597    let service_path = format!("{}/{}", S::SERVICE_NAME, instance);
598    let directory_proxy =
599        fuchsia_fs::directory::open_directory_async(directory, &service_path, fio::Flags::empty())?;
600    Ok(S::Proxy::from_member_opener(Box::new(ServiceInstanceDirectory(
601        directory_proxy,
602        instance.to_string(),
603    ))))
604}
605
606/// Connect to an instance of a FIDL service hosted in `directory`, in the `svc/` subdir.
607pub fn connect_to_service_instance_at_dir_svc<S: ServiceMarker>(
608    directory: &impl AsRefDirectory,
609    instance: impl AsRef<str>,
610) -> Result<S::Proxy, Error> {
611    let service_path = format!("{SVC_DIR}/{}/{}", S::SERVICE_NAME, instance.as_ref());
612    // TODO(https://fxbug.dev/42068248): Some Directory implementations require relative paths,
613    // even though they aren't technically supposed to, so strip the leading slash until that's
614    // resolved one way or the other.
615    let service_path = service_path.strip_prefix('/').unwrap();
616    let directory_proxy = open_directory_async(directory, service_path, fio::Rights::empty())?;
617    Ok(S::Proxy::from_member_opener(Box::new(ServiceInstanceDirectory(
618        directory_proxy,
619        instance.as_ref().to_string(),
620    ))))
621}
622
623/// Opens a FIDL service as a directory, which holds instances of the service.
624pub fn open_service<S: ServiceMarker>() -> Result<fio::DirectoryProxy, Error> {
625    let service_path = format!("{}/{}", SVC_DIR, S::SERVICE_NAME);
626    fuchsia_fs::directory::open_in_namespace(&service_path, fio::Flags::empty())
627        .context("namespace open failed")
628}
629
630/// Opens a FIDL service with a custom name as a directory, which holds instances of the service.
631pub fn open_service_at(service_name: impl AsRef<str>) -> Result<fio::DirectoryProxy, Error> {
632    let service_path = format!("{SVC_DIR}/{}", service_name.as_ref());
633    fuchsia_fs::directory::open_in_namespace(&service_path, fio::Flags::empty())
634        .context("namespace open failed")
635}
636
637/// Opens the exposed directory from a child. Only works in CFv2, and only works if this component
638/// uses `fuchsia.component.Realm`.
639pub async fn open_childs_exposed_directory(
640    child_name: impl Into<String>,
641    collection_name: Option<String>,
642) -> Result<fio::DirectoryProxy, Error> {
643    let realm_proxy = connect_to_protocol::<RealmMarker>()?;
644    let (directory_proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
645    let child_ref = ChildRef { name: child_name.into(), collection: collection_name };
646    realm_proxy.open_exposed_dir(&child_ref, server_end).await?.map_err(|e| {
647        let ChildRef { name, collection } = child_ref;
648        format_err!("failed to bind to child {} in collection {:?}: {:?}", name, collection, e)
649    })?;
650    Ok(directory_proxy)
651}
652
653/// Connects to a FIDL protocol exposed by a child that's within the `/svc` directory. Only works in
654/// CFv2, and only works if this component uses `fuchsia.component.Realm`.
655pub async fn connect_to_childs_protocol<P: DiscoverableProtocolMarker>(
656    child_name: String,
657    collection_name: Option<String>,
658) -> Result<P::Proxy, Error> {
659    let child_exposed_directory =
660        open_childs_exposed_directory(child_name, collection_name).await?;
661    connect_to_protocol_at_dir_root::<P>(&child_exposed_directory)
662}
663
664/// Returns a connection to the Realm protocol. Components v2 only.
665pub fn realm() -> Result<RealmProxy, Error> {
666    connect_to_protocol::<RealmMarker>()
667}
668
669#[cfg(test)]
670mod tests {
671    use std::collections::HashSet;
672    use std::sync::Arc;
673
674    use super::*;
675    use fidl::endpoints::ServiceMarker as _;
676    use fidl_fuchsia_component_client_test::{
677        ProtocolAMarker, ProtocolAProxy, ProtocolBMarker, ProtocolBProxy, ServiceMarker,
678    };
679    use fuchsia_async::{self as fasync};
680    use futures::{TryStreamExt, future};
681    use vfs::directory::simple::Simple;
682    use vfs::file::vmo::read_only;
683    use vfs::pseudo_directory;
684
685    #[fasync::run_singlethreaded(test)]
686    async fn test_svc_connector_svc_does_not_exist() -> Result<(), Error> {
687        let req = new_protocol_connector::<ProtocolAMarker>().context("error probing service")?;
688        assert_matches::assert_matches!(
689            req.exists().await.context("error checking service"),
690            Ok(false)
691        );
692        let _: ProtocolAProxy = req.connect().context("error connecting to service")?;
693
694        let req = new_protocol_connector_at::<ProtocolAMarker>(SVC_DIR)
695            .context("error probing service at svc dir")?;
696        assert_matches::assert_matches!(
697            req.exists().await.context("error checking service at svc dir"),
698            Ok(false)
699        );
700        let _: ProtocolAProxy = req.connect().context("error connecting to service at svc dir")?;
701
702        Ok(())
703    }
704
705    #[fasync::run_singlethreaded(test)]
706    async fn test_svc_connector_connect_with_dir() -> Result<(), Error> {
707        let dir = pseudo_directory! {
708            ProtocolBMarker::PROTOCOL_NAME => read_only("read_only"),
709        };
710        let dir_proxy = vfs::directory::serve_read_only(dir);
711        let req = new_protocol_connector_in_dir::<ProtocolAMarker>(&dir_proxy);
712        assert_matches::assert_matches!(
713            req.exists().await.context("error probing invalid service"),
714            Ok(false)
715        );
716        let _: ProtocolAProxy = req.connect().context("error connecting to invalid service")?;
717
718        let req = new_protocol_connector_in_dir::<ProtocolBMarker>(&dir_proxy);
719        assert_matches::assert_matches!(
720            req.exists().await.context("error probing service"),
721            Ok(true)
722        );
723        let _: ProtocolBProxy = req.connect().context("error connecting to service")?;
724
725        Ok(())
726    }
727
728    fn make_inner_service_instance_tree() -> Arc<Simple> {
729        pseudo_directory! {
730            ServiceMarker::SERVICE_NAME => pseudo_directory! {
731                "default" => read_only("read_only"),
732                "another_instance" => read_only("read_only"),
733            },
734        }
735    }
736
737    fn make_service_instance_tree() -> Arc<Simple> {
738        pseudo_directory! {
739            "svc" => make_inner_service_instance_tree(),
740        }
741    }
742
743    #[fasync::run_until_stalled(test)]
744    async fn test_service_instance_watcher_from_root() -> Result<(), Error> {
745        let dir_proxy = vfs::directory::serve_read_only(make_service_instance_tree());
746        let watcher = Service::open_from_dir_prefix(&dir_proxy, SVC_DIR, ServiceMarker)?;
747        let found_names: HashSet<_> = watcher
748            .watch()
749            .await?
750            .take(2)
751            .and_then(|proxy| future::ready(Ok(proxy.instance_name().to_owned())))
752            .try_collect()
753            .await?;
754
755        assert_eq!(
756            found_names,
757            HashSet::from_iter(["default".to_owned(), "another_instance".to_owned()])
758        );
759
760        Ok(())
761    }
762
763    #[fasync::run_until_stalled(test)]
764    async fn test_service_instance_watcher_from_svc() -> Result<(), Error> {
765        let dir_proxy = vfs::directory::serve_read_only(make_inner_service_instance_tree());
766        let watcher = Service::open_from_dir(&dir_proxy, ServiceMarker)?;
767        let found_names: HashSet<_> = watcher
768            .watch()
769            .await?
770            .take(2)
771            .and_then(|proxy| future::ready(Ok(proxy.instance_name().to_owned())))
772            .try_collect()
773            .await?;
774
775        assert_eq!(
776            found_names,
777            HashSet::from_iter(["default".to_owned(), "another_instance".to_owned()])
778        );
779
780        Ok(())
781    }
782
783    #[fasync::run_until_stalled(test)]
784    async fn test_connect_to_all_services() -> Result<(), Error> {
785        let dir_proxy = vfs::directory::serve_read_only(make_service_instance_tree());
786        let watcher = Service::open_from_dir_prefix(&dir_proxy, SVC_DIR, ServiceMarker)?;
787        let _: Vec<_> = watcher.watch().await?.take(2).try_collect().await?;
788
789        Ok(())
790    }
791
792    #[fasync::run_until_stalled(test)]
793    async fn test_connect_to_any() -> Result<(), Error> {
794        let dir_proxy = vfs::directory::serve_read_only(make_service_instance_tree());
795        let watcher = Service::open_from_dir_prefix(&dir_proxy, SVC_DIR, ServiceMarker)?;
796        let found = watcher.watch_for_any().await?;
797        assert!(["default", "another_instance"].contains(&found.instance_name()));
798
799        Ok(())
800    }
801}