| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! The `element_management` library provides utilities for sessions to service |
| //! incoming [`fidl_fuchsia_element::ManagerRequest`]s. |
| //! |
| //! Elements are instantiated as dynamic component instances in a component collection of the |
| //! calling component. |
| |
| use { |
| async_trait::async_trait, |
| fidl, |
| fidl::endpoints::{DiscoverableService, Proxy, UnifiedServiceMarker}, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_element as felement, |
| fidl_fuchsia_sys as fsys, fidl_fuchsia_sys2 as fsys2, fuchsia_async as fasync, |
| fuchsia_component, |
| fuchsia_syslog::fx_log_err, |
| fuchsia_syslog::fx_log_info, |
| fuchsia_zircon as zx, |
| futures::TryStreamExt, |
| rand::{distributions::Alphanumeric, thread_rng, Rng}, |
| realm_management, |
| std::fmt, |
| thiserror::Error, |
| }; |
| |
| /// Errors returned by calls to [`ElementManager`]. |
| #[derive(Debug, Error, Clone, PartialEq)] |
| pub enum ElementManagerError { |
| /// Returned when the element manager fails to created the component instance associated with |
| /// a given element. |
| #[error("Element spec for \"{}/{}\" missing url.", name, collection)] |
| UrlMissing { name: String, collection: String }, |
| |
| /// Returned when the element manager fails to create an element with additional services for |
| /// the given ServiceList. This may be because: |
| /// - `host_directory` is not set |
| /// - `provider` is set |
| #[error("Element spec for \"{}/{}\" contains an invalid ServiceList", name, collection)] |
| InvalidServiceList { name: String, collection: String }, |
| |
| /// Returned when the element manager fails to create an element that uses a CFv2 component |
| /// because the spec provides a ServiceList with additional services. |
| /// |
| /// `Spec.additional_services` is only supported for CFv1 components. |
| #[error( |
| "Element spec for \"{}/{}\" provides additional_services for a CFv2 component", |
| name, |
| collection |
| )] |
| AdditionalServicesNotSupported { name: String, collection: String }, |
| |
| /// Returned when the element manager fails to created the component instance associated with |
| /// a given element. |
| #[error("Element {} not created at \"{}/{}\": {:?}", url, collection, name, err)] |
| NotCreated { name: String, collection: String, url: String, err: fcomponent::Error }, |
| |
| /// Returned when the element manager fails to launch a component with the fuchsia.sys |
| /// given launcher. This may be due to an issue with the launcher itself (not bound?). |
| #[error("Element {} not launched: {:?}", url, err_str)] |
| NotLaunched { url: String, err_str: String }, |
| |
| /// Returned when the element manager fails to bind to the component instance associated with |
| /// a given element. |
| #[error("Element {} not bound at \"{}/{}\": {:?}", url, collection, name, err)] |
| NotBound { name: String, collection: String, url: String, err: fcomponent::Error }, |
| } |
| |
| impl ElementManagerError { |
| pub fn url_missing( |
| name: impl Into<String>, |
| collection: impl Into<String>, |
| ) -> ElementManagerError { |
| ElementManagerError::UrlMissing { name: name.into(), collection: collection.into() } |
| } |
| |
| pub fn invalid_service_list( |
| name: impl Into<String>, |
| collection: impl Into<String>, |
| ) -> ElementManagerError { |
| ElementManagerError::InvalidServiceList { name: name.into(), collection: collection.into() } |
| } |
| |
| pub fn additional_services_not_supported( |
| name: impl Into<String>, |
| collection: impl Into<String>, |
| ) -> ElementManagerError { |
| ElementManagerError::AdditionalServicesNotSupported { |
| name: name.into(), |
| collection: collection.into(), |
| } |
| } |
| |
| pub fn not_created( |
| name: impl Into<String>, |
| collection: impl Into<String>, |
| url: impl Into<String>, |
| err: impl Into<fcomponent::Error>, |
| ) -> ElementManagerError { |
| ElementManagerError::NotCreated { |
| name: name.into(), |
| collection: collection.into(), |
| url: url.into(), |
| err: err.into(), |
| } |
| } |
| |
| pub fn not_launched(url: impl Into<String>, err_str: impl Into<String>) -> ElementManagerError { |
| ElementManagerError::NotLaunched { url: url.into(), err_str: err_str.into() } |
| } |
| |
| pub fn not_bound( |
| name: impl Into<String>, |
| collection: impl Into<String>, |
| url: impl Into<String>, |
| err: impl Into<fcomponent::Error>, |
| ) -> ElementManagerError { |
| ElementManagerError::NotBound { |
| name: name.into(), |
| collection: collection.into(), |
| url: url.into(), |
| err: err.into(), |
| } |
| } |
| } |
| |
| /// Checks whether the component is a *.cm or not |
| /// |
| /// # Parameters |
| /// - `component_url`: The component url. |
| fn is_v2_component(component_url: &str) -> bool { |
| component_url.ends_with(".cm") |
| } |
| |
| /// Manages the elements associated with a session. |
| #[async_trait] |
| pub trait ElementManager { |
| /// Adds an element to the session. |
| /// |
| /// This method creates the component instance and binds to it, causing it to start running. |
| /// |
| /// # Parameters |
| /// - `spec`: The description of the element to add as a child. |
| /// - `child_name`: The name of the element, must be unique within a session. The name must be |
| /// non-empty, of the form [a-z0-9-_.]. |
| /// |
| /// On success, the [`Element`] is returned back to the session. |
| /// |
| /// # Errors |
| /// If the child cannot be created or bound in the current [`fidl_fuchsia_sys2::Realm`]. In |
| /// particular, it is an error to call [`launch_element`] twice with the same `child_name`. |
| async fn launch_element( |
| &self, |
| spec: felement::Spec, |
| child_name: &str, |
| ) -> Result<Element, ElementManagerError>; |
| |
| /// Handles requests to the [`Manager`] protocol. |
| /// |
| /// # Parameters |
| /// `stream`: The stream that receives [`Manager`] requests. |
| /// |
| /// # Returns |
| /// `Ok` if the request stream has been successfully handled once the client closes |
| /// its connection. `Error` if a FIDL IO error was encountered. |
| async fn handle_requests( |
| &mut self, |
| mut stream: felement::ManagerRequestStream, |
| ) -> Result<(), fidl::Error>; |
| } |
| |
| enum ExposedCapabilities { |
| /// v1 component App |
| App(fuchsia_component::client::App), |
| /// v2 component exposed capabilities directory |
| Directory(zx::Channel), |
| } |
| |
| impl fmt::Debug for ExposedCapabilities { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| ExposedCapabilities::App(_) => write!(f, "CFv1 App"), |
| ExposedCapabilities::Directory(_) => write!(f, "CFv2 exposed capabilities Directory"), |
| } |
| } |
| } |
| |
| /// Represents a component launched by an Element Manager. |
| /// |
| /// The component can be either a v1 component launched by the fuchsia.sys.Launcher, or a v2 |
| /// component launched as a child of the Element Manager's realm. |
| /// |
| /// The Element can be used to connect to services exposed by the underlying v1 or v2 component. |
| #[derive(Debug)] |
| pub struct Element { |
| /// CF v1 or v2 object that manages a `Directory` request channel for requesting services |
| /// exposed by the component. |
| exposed_capabilities: ExposedCapabilities, |
| |
| /// The component URL used to launch the component. Private but printable via "{:?}". |
| url: String, |
| |
| /// v2 component child name, or empty string if not a child of the realm (such as a CFv1 |
| /// component). Private but printable via "{:?}"". |
| name: String, |
| |
| /// v2 component child collection name or empty string if not a child of the realm (such as a |
| /// CFv1 component). Private but printable via "{:?}"". |
| collection: String, |
| } |
| |
| /// A component launched in response to `ElementManager::ProposeElement()`. |
| /// |
| /// A session uses `ElementManager` to launch and return the Element, and can then use the Element |
| /// to connect to exposed capabilities. |
| /// |
| /// An Element composes either a CFv2 component (launched as a child of the `ElementManager`'s |
| /// realm) or a CFv1 component (launched via a fuchsia::sys::Launcher). |
| impl Element { |
| /// Creates an Element from a `fuchsia_component::client::App`. |
| /// |
| /// # Parameters |
| /// - `url`: The launched component URL. |
| /// - `app`: The v1 component wrapped in an App, returned by the launch function. |
| pub fn from_app(app: fuchsia_component::client::App, url: &str) -> Element { |
| Element { |
| exposed_capabilities: ExposedCapabilities::App(app), |
| url: url.to_string(), |
| name: "".to_string(), |
| collection: "".to_string(), |
| } |
| } |
| |
| /// Creates an Element from a component's exposed capabilities directory. |
| /// |
| /// # Parameters |
| /// - `directory_channel`: A channel to the component's `Directory` of exposed capabilities. |
| /// - `name`: The launched component's name. |
| /// - `url`: The launched component URL. |
| /// - `collection`: The launched component's collection name. |
| pub fn from_directory_channel( |
| directory_channel: zx::Channel, |
| name: &str, |
| url: &str, |
| collection: &str, |
| ) -> Element { |
| Element { |
| exposed_capabilities: ExposedCapabilities::Directory(directory_channel), |
| url: url.to_string(), |
| name: name.to_string(), |
| collection: collection.to_string(), |
| } |
| } |
| |
| // # Note |
| // |
| // The methods below are copied from fuchsia_component::client::App in order to offer |
| // services in the same way, but from any `Element`, wrapping either a v1 `App` or a v2 |
| // component's `Directory` of exposed services. |
| |
| /// Returns a reference to the component's `Directory` of exposed capabilities. A session can |
| /// request services, and/or other capabilities, from the Element, using this channel. |
| /// |
| /// # Returns |
| /// A `channel` to the component's `Directory` of exposed capabilities. |
| #[inline] |
| pub fn directory_channel(&self) -> &zx::Channel { |
| match &self.exposed_capabilities { |
| ExposedCapabilities::App(app) => &app.directory_channel(), |
| ExposedCapabilities::Directory(directory_channel) => &directory_channel, |
| } |
| } |
| |
| #[inline] |
| fn service_path_prefix(&self) -> &str { |
| match &self.exposed_capabilities { |
| ExposedCapabilities::App(..) => "", |
| ExposedCapabilities::Directory(..) => "svc/", |
| } |
| } |
| |
| /// Connect to a service provided by the `Element`. |
| /// |
| /// # Type Parameters |
| /// - S: A FIDL service `Marker` type. |
| /// |
| /// # Returns |
| /// - A service `Proxy` matching the `Marker`, or an error if the service is not available from |
| /// the `Element`. |
| #[inline] |
| pub fn connect_to_service<S: DiscoverableService>(&self) -> Result<S::Proxy, anyhow::Error> { |
| let (client_channel, server_channel) = zx::Channel::create()?; |
| self.connect_to_service_with_channel::<S>(server_channel)?; |
| Ok(S::Proxy::from_channel(fasync::Channel::from_channel(client_channel)?)) |
| } |
| |
| /// Connect to a FIDL Unified Service provided by the `Element`. |
| /// |
| /// # Type Parameters |
| /// - US: A FIDL Unified Service `Marker` type. |
| /// |
| /// # Returns |
| /// - A service `Proxy` matching the `Marker`, or an error if the service is not available from |
| /// the `Element`. |
| #[inline] |
| pub fn connect_to_unified_service<US: UnifiedServiceMarker>( |
| &self, |
| ) -> Result<US::Proxy, anyhow::Error> { |
| fuchsia_component::client::connect_to_unified_service_at_dir::<US>( |
| &self.directory_channel(), |
| ) |
| } |
| |
| /// Connect to a service by passing a channel for the server. |
| /// |
| /// # Type Parameters |
| /// - S: A FIDL service `Marker` type. |
| /// |
| /// # Parameters |
| /// - server_channel: The server-side endpoint of a channel pair, to bind to the requested |
| /// service. The caller will interact with the service via the client-side endpoint. |
| /// |
| /// # Returns |
| /// - Result::Ok or an error if the service is not available from the `Element`. |
| #[inline] |
| pub fn connect_to_service_with_channel<S: DiscoverableService>( |
| &self, |
| server_channel: zx::Channel, |
| ) -> Result<(), anyhow::Error> { |
| self.connect_to_named_service_with_channel(S::SERVICE_NAME, server_channel) |
| } |
| |
| /// Connect to a service by name. |
| /// |
| /// # Parameters |
| /// - service_name: A FIDL service by name. |
| /// - server_channel: The server-side endpoint of a channel pair, to bind to the requested |
| /// service. The caller will interact with the service via the client-side endpoint. |
| /// |
| /// # Returns |
| /// - Result::Ok or an error if the service is not available from the `Element`. |
| #[inline] |
| pub fn connect_to_named_service_with_channel( |
| &self, |
| service_name: &str, |
| server_channel: zx::Channel, |
| ) -> Result<(), anyhow::Error> { |
| fdio::service_connect_at( |
| &self.directory_channel(), |
| &(self.service_path_prefix().to_owned() + service_name), |
| server_channel, |
| )?; |
| Ok(()) |
| } |
| } |
| |
| /// A list of services passed to an Element created from a CFv1 component. |
| /// |
| /// This is a subset of fuchsia::sys::ServiceList that only supports a host |
| /// directory, not a ServiceProvider. |
| struct AdditionalServices { |
| /// List of service names. |
| pub names: Vec<String>, |
| |
| /// A channel to the directory hosting the services in `names`. |
| pub host_directory: zx::Channel, |
| } |
| |
| /// A [`SimpleElementManager`] creates and binds elements. |
| /// |
| /// The [`SimpleElementManager`] provides no additional functionality for managing elements (e.g., |
| /// tracking which elements are running, de-duplicating elements, etc.). |
| pub struct SimpleElementManager { |
| /// The realm which this element manager uses to create components. |
| realm: fsys2::RealmProxy, |
| |
| /// The collection in which elements will be launched. |
| /// |
| /// This is only used for elements that have a CFv2 (*.cm) component URL, and has no meaning |
| /// for CFv1 elementes. |
| /// |
| /// The component that is running the `SimpleElementManager` must have a collection |
| /// with the same name in its CML file. |
| collection: String, |
| |
| /// A proxy to the `fuchsia::sys::Launcher` protocol used to create CFv1 components, or an error |
| /// that represents a failure to connect to the protocol. |
| /// |
| /// If a launcher is not provided during intialization, it is requested from the environment. |
| sys_launcher: Result<fsys::LauncherProxy, anyhow::Error>, |
| |
| /// Elements that were launched with no `Controller` provided by the client. |
| /// |
| /// Elements are added to this list to ensure they stay running when the |
| /// client disconnects from `Manager`. |
| uncontrolled_elements: Vec<Element>, |
| } |
| |
| /// An element manager that launches v1 and v2 components, returning them to the caller. |
| impl SimpleElementManager { |
| pub fn new(realm: fsys2::RealmProxy, collection: &str) -> SimpleElementManager { |
| SimpleElementManager { |
| realm, |
| collection: collection.to_string(), |
| sys_launcher: fuchsia_component::client::connect_to_service::<fsys::LauncherMarker>(), |
| uncontrolled_elements: vec![], |
| } |
| } |
| |
| /// Initializer used by tests, to override the default fuchsia::sys::Launcher with a mock |
| /// launcher. |
| pub fn new_with_sys_launcher( |
| realm: fsys2::RealmProxy, |
| collection: &str, |
| sys_launcher: fsys::LauncherProxy, |
| ) -> Self { |
| SimpleElementManager { |
| realm, |
| collection: collection.to_string(), |
| sys_launcher: Ok(sys_launcher), |
| uncontrolled_elements: vec![], |
| } |
| } |
| |
| /// Launches a CFv1 component as an element. |
| /// |
| /// # Parameters |
| /// - `child_url`: The component url of the child added to the session. This function launches |
| /// components using a fuchsia::sys::Launcher. It supports all CFv1 component |
| /// URL schemes, such as URLs starting with `https`, and Fuchsia component URLs |
| /// ending in `.cmx`. Fuchsia components ending in `.cm` should use |
| /// `launch_child_component()` instead. |
| /// - `additional_services`: Additional services to add the new component's namespace under /svc, |
| /// in addition to those coming from the environment. |
| /// |
| /// # Returns |
| /// An Element backed by the CFv1 component. |
| async fn launch_v1_element( |
| &self, |
| child_url: &str, |
| additional_services: Option<AdditionalServices>, |
| ) -> Result<Element, ElementManagerError> { |
| let sys_launcher = (&self.sys_launcher).as_ref().map_err(|err: &anyhow::Error| { |
| ElementManagerError::not_launched( |
| child_url.clone(), |
| format!("Error connecting to fuchsia::sys::Launcher: {}", err.to_string()), |
| ) |
| })?; |
| |
| let mut launch_options = fuchsia_component::client::LaunchOptions::new(); |
| if let Some(services) = additional_services { |
| launch_options.set_additional_services(services.names, services.host_directory); |
| } |
| |
| let app = fuchsia_component::client::launch_with_options( |
| &sys_launcher, |
| child_url.to_string(), |
| None, |
| launch_options, |
| ) |
| .map_err(|err: anyhow::Error| { |
| ElementManagerError::not_launched(child_url.clone(), err.to_string()) |
| })?; |
| |
| Ok(Element::from_app(app, child_url)) |
| } |
| |
| /// Launches a CFv2 component as an element. |
| /// |
| /// The component is created as a child in the Element Manager's realm. |
| /// |
| /// # Parameters |
| /// - `child_name`: The name of the element, must be unique within a session. The name must be |
| /// non-empty, of the form [a-z0-9-_.]. |
| /// - `child_url`: The component url of the child added to the session. |
| /// |
| /// # Returns |
| /// An Element backed by the CFv2 component. |
| async fn launch_v2_element( |
| &self, |
| child_name: &str, |
| child_url: &str, |
| ) -> Result<Element, ElementManagerError> { |
| fx_log_info!( |
| "launch_v2_element(name={}, url={}, collection={})", |
| child_name, |
| child_url, |
| self.collection |
| ); |
| realm_management::create_child_component( |
| &child_name, |
| &child_url, |
| &self.collection, |
| &self.realm, |
| ) |
| .await |
| .map_err(|err: fcomponent::Error| { |
| ElementManagerError::not_created(child_name, &self.collection, child_url, err) |
| })?; |
| |
| let directory_channel = |
| match realm_management::bind_child_component(child_name, &self.collection, &self.realm) |
| .await |
| { |
| Ok(channel) => channel, |
| Err(err) => { |
| return Err(ElementManagerError::not_bound( |
| child_name, |
| self.collection.clone(), |
| child_url, |
| err, |
| )) |
| } |
| }; |
| Ok(Element::from_directory_channel( |
| directory_channel, |
| child_name, |
| child_url, |
| &self.collection, |
| )) |
| } |
| } |
| |
| /// Handles Controller protocol requests. |
| /// |
| /// # Parameters |
| /// - `stream`: the input channel that receives [`Controller`] requests. |
| /// - `element`: the [`Element`] that is being controlled. |
| /// |
| /// # Returns |
| /// () when there are no more valid requests. |
| async fn handle_controller_requests( |
| mut stream: felement::ControllerRequestStream, |
| _element: Element, |
| ) { |
| while let Ok(Some(request)) = stream.try_next().await { |
| match request { |
| felement::ControllerRequest::UpdateAnnotations { |
| annotations_to_set: _, |
| annotations_to_delete: _, |
| responder, |
| } => { |
| fx_log_err!( |
| "TODO(fxbug.dev/65759): Controller.UpdateAnnotations is not implemented", |
| ); |
| responder.control_handle().shutdown_with_epitaph(zx::Status::UNAVAILABLE); |
| } |
| felement::ControllerRequest::GetAnnotations { responder } => { |
| fx_log_err!("TODO(fxbug.dev/65759): Controller.GetAnnotations is not implemented"); |
| responder.control_handle().shutdown_with_epitaph(zx::Status::UNAVAILABLE); |
| } |
| } |
| } |
| } |
| |
| #[async_trait] |
| impl ElementManager for SimpleElementManager { |
| async fn launch_element( |
| &self, |
| spec: felement::Spec, |
| child_name: &str, |
| ) -> Result<Element, ElementManagerError> { |
| let child_url = spec |
| .component_url |
| .ok_or_else(|| ElementManagerError::url_missing(child_name, &self.collection))?; |
| |
| let additional_services = |
| spec.additional_services.map_or(Ok(None), |services| match services { |
| fsys::ServiceList { |
| names, |
| host_directory: Some(service_host_directory), |
| provider: None, |
| } => Ok(Some(AdditionalServices { names, host_directory: service_host_directory })), |
| _ => Err(ElementManagerError::invalid_service_list(child_name, &self.collection)), |
| })?; |
| |
| let element = if is_v2_component(&child_url) { |
| // `additional_services` is only supported for CFv1 components. |
| if additional_services.is_some() { |
| return Err(ElementManagerError::additional_services_not_supported( |
| child_name, |
| &self.collection, |
| )); |
| } |
| |
| self.launch_v2_element(&child_name, &child_url).await? |
| } else { |
| self.launch_v1_element(&child_url, additional_services).await? |
| }; |
| |
| Ok(element) |
| } |
| |
| async fn handle_requests( |
| &mut self, |
| mut stream: felement::ManagerRequestStream, |
| ) -> Result<(), fidl::Error> { |
| while let Some(request) = stream.try_next().await? { |
| match request { |
| felement::ManagerRequest::ProposeElement { spec, controller, responder } => { |
| let mut child_name: String = |
| thread_rng().sample_iter(&Alphanumeric).take(16).collect(); |
| child_name.make_ascii_lowercase(); |
| |
| let mut result = match self.launch_element(spec, &child_name).await { |
| Ok(element) => { |
| match controller { |
| Some(controller) => match controller.into_stream() { |
| Ok(stream) => { |
| fasync::Task::spawn(handle_controller_requests( |
| stream, element, |
| )) |
| .detach(); |
| Ok(()) |
| } |
| Err(err) => { |
| fx_log_err!( |
| "Failed to convert Controller request to stream: {:?}", |
| err |
| ); |
| Err(felement::ProposeElementError::InvalidArgs) |
| } |
| }, |
| // If the element proposer did not provide a controller, add the |
| // element to a vector to keep it alive: |
| None => { |
| self.uncontrolled_elements.push(element); |
| Ok(()) |
| } |
| } |
| } |
| Err(err) => { |
| fx_log_err!("Failed to launch element: {:?}", err); |
| |
| match err { |
| ElementManagerError::UrlMissing { .. } => { |
| Err(felement::ProposeElementError::NotFound) |
| } |
| ElementManagerError::InvalidServiceList { .. } |
| | ElementManagerError::AdditionalServicesNotSupported { .. } |
| | ElementManagerError::NotCreated { .. } |
| | ElementManagerError::NotBound { .. } |
| | ElementManagerError::NotLaunched { .. } => { |
| Err(felement::ProposeElementError::InvalidArgs) |
| } |
| } |
| } |
| }; |
| |
| let _ = responder.send(&mut result); |
| } |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::{ElementManager, ElementManagerError, SimpleElementManager}, |
| fidl::endpoints::{create_proxy_and_stream, ServerEnd}, |
| fidl_fuchsia_component as fcomponent, fidl_fuchsia_element as felement, |
| fidl_fuchsia_io as fio, fidl_fuchsia_sys as fsys, fidl_fuchsia_sys2 as fsys2, |
| fuchsia_async as fasync, fuchsia_zircon as zx, |
| futures::{channel::mpsc::channel, prelude::*}, |
| lazy_static::lazy_static, |
| test_util::Counter, |
| }; |
| |
| /// Spawns a local `fidl_fuchsia_sys2::Realm` server, and returns a proxy to the spawned server. |
| /// The provided `request_handler` is notified when an incoming request is received. |
| /// |
| /// # Parameters |
| /// - `request_handler`: A function which is called with incoming requests to the spawned |
| /// `Realm` server. |
| /// # Returns |
| /// A `RealmProxy` to the spawned server. |
| fn spawn_realm_server<F: 'static>(request_handler: F) -> fsys2::RealmProxy |
| where |
| F: Fn(fsys2::RealmRequest) + Send, |
| { |
| let (realm_proxy, mut realm_server) = create_proxy_and_stream::<fsys2::RealmMarker>() |
| .expect("Failed to create realm proxy and server."); |
| |
| fasync::Task::spawn(async move { |
| while let Some(realm_request) = realm_server.try_next().await.unwrap() { |
| request_handler(realm_request); |
| } |
| }) |
| .detach(); |
| |
| realm_proxy |
| } |
| |
| fn spawn_directory_server<F: 'static>( |
| mut directory_server: fio::DirectoryRequestStream, |
| request_handler: F, |
| ) where |
| F: Fn(fio::DirectoryRequest) + Send, |
| { |
| fasync::Task::spawn(async move { |
| while let Some(directory_request) = directory_server.try_next().await.unwrap() { |
| request_handler(directory_request); |
| } |
| }) |
| .detach(); |
| } |
| |
| /// Spawns a local `fidl_fuchsia_sys::Launcher` server, and returns a proxy to the spawned |
| /// server. The provided `request_handler` is notified when an incoming request is received. |
| /// |
| /// # Parameters |
| /// - `request_handler`: A function which is called with incoming requests to the spawned |
| /// `Launcher` server. |
| /// # Returns |
| /// A `LauncherProxy` to the spawned server. |
| fn spawn_launcher_server<F: 'static>(request_handler: F) -> fsys::LauncherProxy |
| where |
| F: Fn(fsys::LauncherRequest) + Send, |
| { |
| let (launcher_proxy, mut launcher_server) = |
| create_proxy_and_stream::<fsys::LauncherMarker>() |
| .expect("Failed to create launcher proxy and server."); |
| |
| fasync::Task::spawn(async move { |
| while let Some(launcher_request) = launcher_server.try_next().await.unwrap() { |
| request_handler(launcher_request); |
| } |
| }) |
| .detach(); |
| |
| launcher_proxy |
| } |
| |
| /// Tests that launching a component with a cmx file successfully returns an Element |
| /// with outgoing directory routing appropriate for v1 components. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_v1_element_success() { |
| lazy_static! { |
| static ref CREATE_COMPONENT_CALL_COUNT: Counter = Counter::new(0); |
| } |
| |
| const ELEMENT_COUNT: usize = 1; |
| |
| let component_url = "test_url.cmx"; |
| let child_name = "child"; |
| let child_collection = "elements"; |
| |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| _ => { |
| // CFv1 elements do not use the realm so fail the test if it is requested. |
| assert!(false); |
| } |
| }); |
| |
| let (directory_open_sender, directory_open_receiver) = channel::<String>(1); |
| let directory_request_handler = move |directory_request| match directory_request { |
| fio::DirectoryRequest::Open { path: capability_path, .. } => { |
| let mut result_sender = directory_open_sender.clone(); |
| fasync::Task::spawn(async move { |
| let _ = result_sender.send(capability_path).await; |
| }) |
| .detach() |
| } |
| _ => { |
| assert!(false); |
| } |
| }; |
| |
| let (create_component_sender, create_component_receiver) = channel::<()>(ELEMENT_COUNT); |
| let launcher = spawn_launcher_server(move |launcher_request| match launcher_request { |
| fsys::LauncherRequest::CreateComponent { |
| launch_info: fsys::LaunchInfo { url, directory_request, .. }, |
| .. |
| } => { |
| assert_eq!(url, component_url); |
| let mut result_sender = create_component_sender.clone(); |
| spawn_directory_server( |
| ServerEnd::<fio::DirectoryMarker>::new(directory_request.unwrap()) |
| .into_stream() |
| .unwrap(), |
| directory_request_handler.clone(), |
| ); |
| fasync::Task::spawn(async move { |
| let _ = result_sender.send(()).await; |
| CREATE_COMPONENT_CALL_COUNT.inc(); |
| }) |
| .detach() |
| } |
| }); |
| |
| let element_manager = |
| SimpleElementManager::new_with_sys_launcher(realm, child_collection, launcher); |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| child_name, |
| ) |
| .await; |
| let element = result.unwrap(); |
| assert!(format!("{:?}", element).contains(component_url)); |
| |
| // Verify that the CreateComponent was actually called. |
| create_component_receiver.into_future().await; |
| assert_eq!(CREATE_COMPONENT_CALL_COUNT.get(), ELEMENT_COUNT); |
| |
| // Now use the element api to open a service in the element's outgoing dir. Verify |
| // that the directory channel received the request with the correct path. |
| let (_client_channel, server_channel) = zx::Channel::create().unwrap(); |
| let _ = element.connect_to_named_service_with_channel("myService", server_channel); |
| let open_paths = directory_open_receiver.take(1).collect::<Vec<_>>().await; |
| // While a standard v1 component publishes its services to "svc/...", LaunchInfo.directory_request, |
| // which is mocked in the test setup, points to the "svc" subdirectory in production. This is why |
| // the expected path does not contain the "svc/" prefix. |
| assert_eq!(vec!["myService"], open_paths); |
| } |
| |
| /// Tests that launching multiple v1 components (without "*.cm") successfully returns [`Ok`]. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_multiple_v1_element_success() { |
| lazy_static! { |
| static ref CREATE_COMPONENT_CALL_COUNT: Counter = Counter::new(0); |
| } |
| |
| const ELEMENT_COUNT: usize = 2; |
| |
| let (sender, receiver) = channel::<()>(ELEMENT_COUNT); |
| |
| let a_component_url = "a_url.cmx"; |
| let a_child_name = "a_child"; |
| |
| let b_component_url = "https://google.com"; |
| let b_child_name = "b_child"; |
| |
| let child_collection = "elements"; |
| |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| _ => { |
| // CFv1 elements do not use the realm so fail the test if it is requested. |
| assert!(false); |
| } |
| }); |
| |
| let launcher = spawn_launcher_server(move |launcher_request| match launcher_request { |
| fsys::LauncherRequest::CreateComponent { |
| launch_info: fsys::LaunchInfo { .. }, .. |
| } => { |
| let mut result_sender = sender.clone(); |
| fasync::Task::spawn(async move { |
| let _ = result_sender.send(()).await.expect("Could not create component."); |
| CREATE_COMPONENT_CALL_COUNT.inc(); |
| }) |
| .detach() |
| } |
| }); |
| |
| let element_manager = |
| SimpleElementManager::new_with_sys_launcher(realm, child_collection, launcher); |
| assert!(element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(a_component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| a_child_name, |
| ) |
| .await |
| .is_ok()); |
| assert!(element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(b_component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| b_child_name, |
| ) |
| .await |
| .is_ok()); |
| |
| // Verify that the CreateComponent was actually called. |
| receiver.into_future().await; |
| assert_eq!(CREATE_COMPONENT_CALL_COUNT.get(), ELEMENT_COUNT); |
| } |
| |
| /// Tests that launching an element with a *.cmx URL and `additional_services` ServiceList |
| /// passes the services to the component Launcher. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_v1_element_with_additional_services() { |
| lazy_static! { |
| static ref CREATE_COMPONENT_CALL_COUNT: Counter = Counter::new(0); |
| } |
| |
| let component_url = "test_url.cmx"; |
| let child_name = "child"; |
| let child_collection = "elements"; |
| |
| const ELEMENT_COUNT: usize = 1; |
| |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| _ => { |
| // CFv1 elements do not use the realm so fail the test if it is requested. |
| assert!(false); |
| } |
| }); |
| |
| // Spawn a directory server from which the element can connect to services. |
| let (directory_open_sender, directory_open_receiver) = channel::<String>(1); |
| let directory_request_handler = move |directory_request| match directory_request { |
| fio::DirectoryRequest::Open { path: capability_path, .. } => { |
| let mut result_sender = directory_open_sender.clone(); |
| fasync::Task::spawn(async move { |
| let _ = result_sender.send(capability_path).await; |
| }) |
| .detach() |
| } |
| _ => { |
| assert!(false); |
| } |
| }; |
| |
| let (dir_client, dir_server) = fidl::Channel::create().unwrap(); |
| spawn_directory_server( |
| ServerEnd::<fio::DirectoryMarker>::new(dir_server).into_stream().unwrap(), |
| directory_request_handler.clone(), |
| ); |
| |
| // The element receives the client side handle to the directory |
| // through `additional_services.host_directory`. |
| let service_name = "myService"; |
| let additional_services = fsys::ServiceList { |
| names: vec![service_name.to_string()], |
| host_directory: Some(dir_client), |
| provider: None, |
| }; |
| |
| let (create_component_sender, create_component_receiver) = channel::<()>(ELEMENT_COUNT); |
| let launcher = spawn_launcher_server(move |launcher_request| match launcher_request { |
| fsys::LauncherRequest::CreateComponent { |
| launch_info: fsys::LaunchInfo { url, additional_services, .. }, |
| .. |
| } => { |
| assert_eq!(url, component_url); |
| |
| assert!(additional_services.is_some()); |
| let services = additional_services.unwrap(); |
| assert!(services.host_directory.is_some()); |
| let host_directory = services.host_directory.unwrap(); |
| |
| assert_eq!(vec![service_name.to_string()], services.names); |
| |
| // Connect to the service hosted in `additional_services.host_directory`. |
| let (_client_channel, server_channel) = zx::Channel::create().unwrap(); |
| fdio::service_connect_at(&host_directory, service_name, server_channel) |
| .expect("could not connect to service"); |
| |
| let mut result_sender = create_component_sender.clone(); |
| fasync::Task::spawn(async move { |
| let _ = result_sender.send(()).await; |
| CREATE_COMPONENT_CALL_COUNT.inc(); |
| }) |
| .detach() |
| } |
| }); |
| |
| let element_manager = |
| SimpleElementManager::new_with_sys_launcher(realm, child_collection, launcher); |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| additional_services: Some(additional_services), |
| ..felement::Spec::EMPTY |
| }, |
| child_name, |
| ) |
| .await; |
| let element = result.unwrap(); |
| assert!(format!("{:?}", element).contains(component_url)); |
| |
| // Verify that the CreateComponent was actually called. |
| create_component_receiver.into_future().await; |
| assert_eq!(CREATE_COMPONENT_CALL_COUNT.get(), ELEMENT_COUNT); |
| |
| // Verify that the element opened a path in `host_directory` that matches the |
| // service name as a result of connecting to the service. |
| let open_paths = directory_open_receiver.take(1).collect::<Vec<_>>().await; |
| assert_eq!(vec![service_name], open_paths); |
| } |
| |
| /// Tests that launching a *.cm element successfully returns an Element with |
| /// outgoing directory routing appropriate for v2 components. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_v2_element_success() { |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cm"; |
| let child_name = "child"; |
| let child_collection = "elements"; |
| |
| let (directory_open_sender, directory_open_receiver) = channel::<String>(1); |
| let directory_request_handler = move |directory_request| match directory_request { |
| fio::DirectoryRequest::Open { path: capability_path, .. } => { |
| let mut result_sender = directory_open_sender.clone(); |
| fasync::Task::spawn(async move { |
| let _ = result_sender.send(capability_path).await; |
| }) |
| .detach() |
| } |
| _ => { |
| assert!(false); |
| } |
| }; |
| |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| fsys2::RealmRequest::CreateChild { collection, decl, responder } => { |
| assert_eq!(decl.url.unwrap(), component_url); |
| assert_eq!(decl.name.unwrap(), child_name); |
| assert_eq!(&collection.name, child_collection); |
| |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys2::RealmRequest::BindChild { child, exposed_dir, responder } => { |
| assert_eq!(child.collection, Some(child_collection.to_string())); |
| spawn_directory_server( |
| exposed_dir.into_stream().unwrap(), |
| directory_request_handler.clone(), |
| ); |
| let _ = responder.send(&mut Ok(())); |
| } |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, child_collection); |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| child_name, |
| ) |
| .await; |
| let element = result.unwrap(); |
| |
| // Now use the element api to open a service in the element's outgoing dir. Verify |
| // that the directory channel received the request with the correct path. |
| let (_client_channel, server_channel) = zx::Channel::create().unwrap(); |
| let _ = element.connect_to_named_service_with_channel("myService", server_channel); |
| let open_paths = directory_open_receiver.take(1).collect::<Vec<_>>().await; |
| assert_eq!(vec!["svc/myService"], open_paths); |
| } |
| |
| /// Tests that adding a .cm element does not use fuchsia.sys.Launcher. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_element_success_not_use_launcher() { |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cm"; |
| let child_name = "child"; |
| let child_collection = "elements"; |
| |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| fsys2::RealmRequest::CreateChild { collection, decl, responder } => { |
| assert_eq!(decl.url.unwrap(), component_url); |
| assert_eq!(decl.name.unwrap(), child_name); |
| assert_eq!(&collection.name, child_collection); |
| |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys2::RealmRequest::BindChild { child, exposed_dir: _, responder } => { |
| assert_eq!(child.collection, Some(child_collection.to_string())); |
| |
| let _ = responder.send(&mut Ok(())); |
| } |
| _ => { |
| assert!(false); |
| } |
| }); |
| let launcher = spawn_launcher_server(move |launcher_request| match launcher_request { |
| // Fail if any call to the launcher is made. |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = |
| SimpleElementManager::new_with_sys_launcher(realm, child_collection, launcher); |
| assert!(element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| child_name, |
| ) |
| .await |
| .is_ok()); |
| } |
| |
| /// Tests that launching an element with no URL returns the appropriate error. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_element_no_url() { |
| // The following match errors if it sees a bind request: since the child was not created |
| // successfully the bind should not be called. |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, ""); |
| |
| let result = element_manager |
| .launch_element(felement::Spec { component_url: None, ..felement::Spec::EMPTY }, "") |
| .await; |
| assert!(result.is_err()); |
| assert_eq!(result.err().unwrap(), ElementManagerError::url_missing("", "")); |
| } |
| |
| /// Tests that launching a CFv1 element with a ServiceList that specifies a `provider` |
| /// returns `ElementManagerError::InvalidServiceList`. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_v1_element_with_service_list_provider() { |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cmx"; |
| |
| let (channel, _) = fidl::Channel::create().unwrap(); |
| let provider = fidl::endpoints::ClientEnd::<fsys::ServiceProviderMarker>::new(channel); |
| |
| // This ServiceList is invalid because it specifies a `provider` that is not None. |
| let additional_services = fsys::ServiceList { |
| names: vec!["fuchsia.service.Foo".to_string()], |
| host_directory: None, |
| provider: Some(provider), |
| }; |
| |
| // The following match errors if it sees a bind request: since the child was not created |
| // successfully the bind should not be called. |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, ""); |
| |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| additional_services: Some(additional_services), |
| ..felement::Spec::EMPTY |
| }, |
| "", |
| ) |
| .await; |
| assert!(result.is_err()); |
| assert_eq!(result.err().unwrap(), ElementManagerError::invalid_service_list("", "")); |
| } |
| |
| /// Tests that launching a *.cm element with a spec that contains `additional_services` |
| /// returns `ProposeElementError::AdditionalServicesNotSupported` |
| #[fasync::run_until_stalled(test)] |
| async fn launch_v2_element_with_additional_services() { |
| // This is a CFv2 component URL. `additional_services` is only supported for v1 components. |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cm"; |
| |
| let (channel, _) = fidl::Channel::create().unwrap(); |
| let additional_services = fsys::ServiceList { |
| names: vec!["fuchsia.service.Foo".to_string()], |
| host_directory: Some(channel), |
| provider: None, |
| }; |
| |
| // The following match errors if it sees a bind request: since the child was not created |
| // successfully the bind should not be called. |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, ""); |
| |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| additional_services: Some(additional_services), |
| ..felement::Spec::EMPTY |
| }, |
| "", |
| ) |
| .await; |
| assert!(result.is_err()); |
| assert_eq!( |
| result.err().unwrap(), |
| ElementManagerError::additional_services_not_supported("", "") |
| ); |
| } |
| |
| /// Tests that launching an element which is not successfully created in the realm returns an |
| /// appropriate error. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_element_create_error_internal() { |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cm"; |
| |
| // The following match errors if it sees a bind request: since the child was not created |
| // successfully the bind should not be called. |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| fsys2::RealmRequest::CreateChild { collection: _, decl: _, responder } => { |
| let _ = responder.send(&mut Err(fcomponent::Error::Internal)); |
| } |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, ""); |
| |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| "", |
| ) |
| .await; |
| assert!(result.is_err()); |
| assert_eq!( |
| result.err().unwrap(), |
| ElementManagerError::not_created("", "", component_url, fcomponent::Error::Internal) |
| ); |
| } |
| |
| /// Tests that adding an element which is not successfully created in the realm returns an |
| /// appropriate error. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_element_create_error_no_space() { |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cm"; |
| |
| // The following match errors if it sees a bind request: since the child was not created |
| // successfully the bind should not be called. |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| fsys2::RealmRequest::CreateChild { collection: _, decl: _, responder } => { |
| let _ = responder.send(&mut Err(fcomponent::Error::ResourceUnavailable)); |
| } |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, ""); |
| |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| "", |
| ) |
| .await; |
| assert!(result.is_err()); |
| assert_eq!( |
| result.err().unwrap(), |
| ElementManagerError::not_created( |
| "", |
| "", |
| component_url, |
| fcomponent::Error::ResourceUnavailable |
| ) |
| ); |
| } |
| |
| /// Tests that adding an element which is not successfully bound in the realm returns an |
| /// appropriate error. |
| #[fasync::run_until_stalled(test)] |
| async fn launch_element_bind_error() { |
| let component_url = "fuchsia-pkg://fuchsia.com/simple_element#meta/simple_element.cm"; |
| |
| // The following match errors if it sees a bind request: since the child was not created |
| // successfully the bind should not be called. |
| let realm = spawn_realm_server(move |realm_request| match realm_request { |
| fsys2::RealmRequest::CreateChild { collection: _, decl: _, responder } => { |
| let _ = responder.send(&mut Ok(())); |
| } |
| fsys2::RealmRequest::BindChild { child: _, exposed_dir: _, responder } => { |
| let _ = responder.send(&mut Err(fcomponent::Error::InstanceCannotStart)); |
| } |
| _ => { |
| assert!(false); |
| } |
| }); |
| let element_manager = SimpleElementManager::new(realm, ""); |
| |
| let result = element_manager |
| .launch_element( |
| felement::Spec { |
| component_url: Some(component_url.to_string()), |
| ..felement::Spec::EMPTY |
| }, |
| "", |
| ) |
| .await; |
| assert!(result.is_err()); |
| assert_eq!( |
| result.err().unwrap(), |
| ElementManagerError::not_bound( |
| "", |
| "", |
| component_url, |
| fcomponent::Error::InstanceCannotStart |
| ) |
| ); |
| } |
| } |