| // 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. |
| |
| use { |
| crate::{ |
| builtin::runner::BuiltinRunnerFactory, |
| builtin_environment::{BuiltinEnvironment, BuiltinEnvironmentBuilder}, |
| config::{CapabilityAllowlistKey, RuntimeConfig, SecurityPolicy}, |
| klog, |
| model::{ |
| binding::Binder, |
| component::BindReason, |
| component_id_index::ComponentInstanceId, |
| error::ModelError, |
| events::{event::EventMode, registry::EventSubscription}, |
| hooks::HooksRegistration, |
| model::Model, |
| testing::{echo_service::*, mocks::*, out_dir::OutDir, test_helpers::*}, |
| }, |
| startup::Arguments, |
| }, |
| cm_rust::*, |
| cm_types::Url, |
| fidl::{ |
| self, |
| endpoints::{self, create_proxy, ClientEnd, Proxy, ServerEnd}, |
| }, |
| fidl_fidl_examples_echo::{self as echo}, |
| fidl_fuchsia_component_runner as fcrunner, |
| fidl_fuchsia_io::{ |
| DirectoryProxy, FileEvent, FileMarker, FileObject, FileProxy, NodeInfo, NodeMarker, |
| CLONE_FLAG_SAME_RIGHTS, MODE_TYPE_DIRECTORY, MODE_TYPE_FILE, MODE_TYPE_SERVICE, |
| OPEN_FLAG_CREATE, OPEN_FLAG_DESCRIBE, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE, |
| }, |
| fidl_fuchsia_sys2 as fsys, fuchsia_zircon as zx, |
| futures::lock::Mutex, |
| futures::prelude::*, |
| matches::assert_matches, |
| moniker::{AbsoluteMoniker, PartialMoniker, RelativeMoniker}, |
| std::{ |
| collections::{HashMap, HashSet}, |
| convert::{TryFrom, TryInto}, |
| default::Default, |
| fs, |
| path::{Path, PathBuf}, |
| sync::Arc, |
| }, |
| tempfile::TempDir, |
| vfs::directory::entry::DirectoryEntry, |
| }; |
| |
| /// Construct a capability path for the hippo service. |
| pub fn default_service_capability() -> CapabilityPath { |
| "/svc/hippo".try_into().unwrap() |
| } |
| |
| /// Construct a capability path for the hippo directory. |
| pub fn default_directory_capability() -> CapabilityPath { |
| "/data/hippo".try_into().unwrap() |
| } |
| |
| #[derive(Debug, PartialEq, Clone)] |
| pub enum ExpectedResult { |
| Ok, |
| Err(zx::Status), |
| ErrWithNoEpitaph, |
| } |
| |
| pub enum CheckUse { |
| Protocol { |
| path: CapabilityPath, |
| expected_res: ExpectedResult, |
| }, |
| Directory { |
| path: CapabilityPath, |
| file: PathBuf, |
| expected_res: ExpectedResult, |
| }, |
| Storage { |
| path: CapabilityPath, |
| // The relative moniker from the storage declaration to the use declaration. Only |
| // used if `expected_res` is Ok. |
| storage_relation: Option<RelativeMoniker>, |
| // The backing directory for this storage is in component manager's namsepace, not the |
| // test's isolated test directory. |
| from_cm_namespace: bool, |
| storage_subdir: Option<String>, |
| expected_res: ExpectedResult, |
| }, |
| StorageAdmin { |
| // The relative moniker from the storage declaration to the use declaration. |
| storage_relation: RelativeMoniker, |
| // The backing directory for this storage is in component manager's namsepace, not the |
| // test's isolated test directory. |
| from_cm_namespace: bool, |
| |
| storage_subdir: Option<String>, |
| expected_res: ExpectedResult, |
| }, |
| Event { |
| requests: Vec<EventSubscription>, |
| expected_res: ExpectedResult, |
| }, |
| } |
| |
| impl CheckUse { |
| pub fn default_directory(expected_res: ExpectedResult) -> Self { |
| Self::Directory { |
| path: default_directory_capability(), |
| file: PathBuf::from("hippo"), |
| expected_res, |
| } |
| } |
| } |
| |
| /// Builder for setting up a new `RoutingTest` instance with a non-standard setup. |
| /// |
| /// Use as follows: |
| /// |
| /// ``` |
| /// let universe = RoutingTestBuilder::new("root", components) |
| /// .add_hooks(...) |
| /// .add_outgoing_path(...) |
| /// .build(); |
| /// ``` |
| #[derive(Default)] |
| pub struct RoutingTestBuilder { |
| root_component: String, |
| components: Vec<(&'static str, ComponentDecl)>, |
| additional_hooks: Vec<HooksRegistration>, |
| outgoing_paths: HashMap<String, HashMap<CapabilityPath, Arc<dyn DirectoryEntry>>>, |
| builtin_runners: HashMap<CapabilityName, Arc<dyn BuiltinRunnerFactory>>, |
| namespace_capabilities: Vec<CapabilityDecl>, |
| component_id_index_path: Option<String>, |
| // Map of components that have a custom function serving the "outgoing" directory. |
| // Other functions will receive a stock directory generated by `RoutingTest`. |
| custom_outgoing_host_fns: HashMap<String, HostFn>, |
| capability_policy: HashMap<CapabilityAllowlistKey, HashSet<AbsoluteMoniker>>, |
| } |
| |
| impl RoutingTestBuilder { |
| pub fn new(root_component: &str, components: Vec<(&'static str, ComponentDecl)>) -> Self { |
| RoutingTestBuilder { |
| root_component: root_component.to_string(), |
| components, |
| ..Default::default() |
| } |
| } |
| |
| pub fn add_hooks(mut self, mut hooks: Vec<HooksRegistration>) -> Self { |
| self.additional_hooks.append(&mut hooks); |
| self |
| } |
| |
| /// Expose the given `DirectoryEntry` at the given path of the `component`'s outgoing |
| /// directory. |
| pub fn add_outgoing_path( |
| mut self, |
| component: &str, |
| path: CapabilityPath, |
| entry: Arc<dyn DirectoryEntry>, |
| ) -> Self { |
| self.outgoing_paths |
| .entry(component.to_string()) |
| .or_insert_with(|| HashMap::new()) |
| .insert(path, entry); |
| self |
| } |
| |
| /// Add the given runner as a "builtin runner", registered in the root's environment |
| /// under the given name. |
| pub fn add_builtin_runner(mut self, name: &str, runner: Arc<dyn BuiltinRunnerFactory>) -> Self { |
| self.builtin_runners.insert(name.into(), runner); |
| self |
| } |
| |
| /// Request a custom outgoing directory host function. |
| pub fn set_component_outgoing_host_fn(mut self, component: &str, function: HostFn) -> Self { |
| self.custom_outgoing_host_fns.insert(component.to_string(), function); |
| self |
| } |
| |
| pub fn set_namespace_capabilities(mut self, caps: Vec<CapabilityDecl>) -> Self { |
| self.namespace_capabilities = caps; |
| self |
| } |
| |
| pub fn set_component_id_index_path(mut self, index_path: String) -> Self { |
| self.component_id_index_path = Some(index_path); |
| self |
| } |
| |
| /// Add a custom capability security policy to restrict routing of certain caps. |
| pub fn add_capability_policy( |
| mut self, |
| key: CapabilityAllowlistKey, |
| allowlist: HashSet<AbsoluteMoniker>, |
| ) -> Self { |
| self.capability_policy.insert(key, allowlist); |
| self |
| } |
| |
| pub async fn build(self) -> RoutingTest { |
| RoutingTest::from_builder(self).await |
| } |
| } |
| |
| /// A test for capability routing. |
| /// |
| /// All string arguments are referring to component names, not URLs, ex: "a", not "test:///a" or |
| /// "test:///a_resolved". |
| pub struct RoutingTest { |
| components: Vec<(&'static str, ComponentDecl)>, |
| pub model: Arc<Model>, |
| pub builtin_environment: BuiltinEnvironment, |
| _echo_service: Arc<EchoService>, |
| pub mock_runner: Arc<MockRunner>, |
| test_dir: TempDir, |
| pub test_dir_proxy: DirectoryProxy, |
| root_component_name: String, |
| } |
| |
| impl RoutingTest { |
| /// Initializes a new RoutingTest with a default setup. |
| pub async fn new(root_component: &str, components: Vec<(&'static str, ComponentDecl)>) -> Self { |
| RoutingTestBuilder::new(root_component, components).build().await |
| } |
| |
| /// Construct a new `RoutingTest` from the given builder. |
| async fn from_builder(mut builder: RoutingTestBuilder) -> Self { |
| // Ensure that kernel logging has been set up |
| klog::KernelLogger::init(); |
| |
| let mock_runner = Arc::new(MockRunner::new()); |
| |
| let test_dir = TempDir::new_in("/tmp").expect("failed to create temp directory"); |
| |
| // Create a directory for the components, starting with a single static file |
| // "foo/hippo" in it. |
| let test_dir_proxy = io_util::open_directory_in_namespace( |
| test_dir.path().to_str().unwrap(), |
| io_util::OPEN_RIGHT_READABLE | io_util::OPEN_RIGHT_WRITABLE, |
| ) |
| .expect("failed to open temp directory"); |
| capability_util::create_static_file(&test_dir_proxy, Path::new("foo/hippo"), "hello") |
| .await |
| .expect("could not create test file"); |
| |
| // Create and populate an outgoing directory for each component. |
| let mut mock_resolver = MockResolver::new(); |
| for (name, decl) in &builder.components { |
| let host_fn = match builder.custom_outgoing_host_fns.remove(*name) { |
| // If a custom outgoing HostFn was provided, use that. |
| Some(host_fn) => host_fn, |
| // Otherwise, create a default HostFn filled with sample data. |
| None => Self::build_outgoing_dir( |
| decl, |
| &test_dir_proxy, |
| builder.outgoing_paths.remove(name as &str).unwrap_or_else(|| HashMap::new()), |
| ) |
| .host_fn(), |
| }; |
| mock_runner.add_host_fn(&format!("test:///{}_resolved", name), host_fn); |
| mock_resolver.add_component(name, decl.clone()); |
| } |
| |
| let echo_service = Arc::new(EchoService::new()); |
| |
| // Set up runners for the system, including a default runner "test_runner" |
| // backed by mock_runner. |
| let args = Arguments { |
| root_component_url: Some( |
| Url::new(format!("test:///{}", builder.root_component)).unwrap(), |
| ), |
| ..Default::default() |
| }; |
| let config = RuntimeConfig { |
| namespace_capabilities: builder.namespace_capabilities, |
| security_policy: SecurityPolicy { |
| capability_policy: builder.capability_policy, |
| ..Default::default() |
| }, |
| component_id_index_path: builder.component_id_index_path, |
| ..Default::default() |
| }; |
| let mut env_builder = BuiltinEnvironmentBuilder::new() |
| .set_args(args) |
| .set_runtime_config(config) |
| .add_resolver("test".to_string(), Box::new(mock_resolver)) |
| .add_runner(TEST_RUNNER_NAME.into(), mock_runner.clone()); |
| for (name, runner) in builder.builtin_runners { |
| env_builder = env_builder.add_runner(name, runner); |
| } |
| let builtin_environment = |
| env_builder.build().await.expect("builtin environment setup failed"); |
| |
| let model = builtin_environment.model.clone(); |
| model.root.hooks.install(builder.additional_hooks).await; |
| model.root.hooks.install(echo_service.hooks()).await; |
| |
| Self { |
| components: builder.components, |
| model, |
| builtin_environment, |
| _echo_service: echo_service, |
| mock_runner, |
| test_dir, |
| test_dir_proxy, |
| root_component_name: builder.root_component, |
| } |
| } |
| |
| pub fn test_dir_path(&self) -> &Path { |
| self.test_dir.path() |
| } |
| |
| /// Creates a static file at the given path in the temp directory. |
| pub async fn create_static_file( |
| &self, |
| path: &Path, |
| contents: &str, |
| ) -> Result<(), anyhow::Error> { |
| capability_util::create_static_file(&self.test_dir_proxy, path, contents).await |
| } |
| |
| /// Set up the given OutDir, installing a set of files assumed to exist by |
| /// many tests: |
| /// - A file `/svc/foo` implementing `fidl.examples.echo.Echo`. |
| /// - A static file `/svc/file`, containing the string "hippos" encoded as UTF-8. |
| pub fn install_default_out_files(dir: &mut OutDir) { |
| // Add "/svc/foo", providing an echo server. |
| dir.add_echo_service(CapabilityPath::try_from("/svc/foo").unwrap()); |
| |
| // Add "/svc/file", providing a read-only file. |
| dir.add_static_file(CapabilityPath::try_from("/svc/file").unwrap(), "hippos"); |
| } |
| |
| /// Creates a dynamic child `child_decl` in `moniker`'s `collection`. |
| pub async fn create_dynamic_child<'a>( |
| &'a self, |
| moniker: AbsoluteMoniker, |
| collection: &'a str, |
| decl: ChildDecl, |
| ) { |
| let component_name = |
| self.bind_instance_and_wait_start(&moniker).await.expect("bind instance failed"); |
| let component_resolved_url = Self::resolved_url(&component_name); |
| Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await; |
| let namespace = self |
| .mock_runner |
| .get_namespace(&component_resolved_url) |
| .expect("could not find child namespace"); |
| capability_util::call_create_child(&namespace, collection, decl).await; |
| } |
| |
| /// Deletes a dynamic child `child_decl` in `moniker`'s `collection`, waiting for destruction |
| /// to complete. |
| pub async fn destroy_dynamic_child<'a>( |
| &'a self, |
| moniker: AbsoluteMoniker, |
| collection: &'a str, |
| name: &'a str, |
| ) { |
| let component = self.model.look_up(&moniker).await.expect("failed to look up component"); |
| self.model |
| .bind(&component.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("bind instance failed"); |
| let partial_moniker = PartialMoniker::new(name.to_string(), Some(collection.to_string())); |
| let nf = |
| component.remove_dynamic_child(&partial_moniker).await.expect("failed to remove child"); |
| // Wait for destruction to fully complete. |
| nf.await.expect("failed to destroy child"); |
| } |
| |
| /// Creates a sub directory in the outgoing dir's /data directory |
| pub fn add_subdir_to_data_directory(&self, subdir: &str) { |
| fs::create_dir_all(self.test_dir.path().join(subdir)).unwrap() |
| } |
| |
| /// Checks a `use` declaration at `moniker` by trying to use `capability`. |
| pub async fn check_use(&self, moniker: AbsoluteMoniker, check: CheckUse) { |
| let component_name = |
| self.bind_instance_and_wait_start(&moniker).await.expect("bind instance failed"); |
| let component_resolved_url = Self::resolved_url(&component_name); |
| let namespace = self |
| .mock_runner |
| .get_namespace(&component_resolved_url) |
| .expect("could not find child namespace"); |
| Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await; |
| match check { |
| CheckUse::Protocol { path, expected_res } => { |
| capability_util::call_echo_svc_from_namespace(&namespace, path, expected_res).await; |
| } |
| CheckUse::Directory { path, file, expected_res } => { |
| capability_util::read_data_from_namespace(&namespace, path, &file, expected_res) |
| .await |
| } |
| CheckUse::Storage { |
| path, |
| storage_relation, |
| from_cm_namespace, |
| storage_subdir, |
| expected_res, |
| } => { |
| if let ExpectedResult::Ok = &expected_res { |
| assert!( |
| storage_relation.is_some(), |
| "relative moniker required if expected result is ok" |
| ); |
| } |
| capability_util::write_file_to_storage(&namespace, path, expected_res.clone()) |
| .await; |
| |
| let instance_id = self |
| .model |
| .root |
| .try_get_context() |
| .unwrap() |
| .component_id_index() |
| .look_up_moniker(&moniker) |
| .cloned(); |
| |
| if let Some(relative_moniker) = storage_relation { |
| if from_cm_namespace { |
| // Check for the file in the /tmp in the test's namespace |
| let tmp_proxy = io_util::open_directory_in_namespace( |
| "/tmp", |
| io_util::OPEN_RIGHT_READABLE, |
| ) |
| .expect("failed to open /tmp"); |
| let res = capability_util::check_file_in_storage( |
| storage_subdir, |
| relative_moniker, |
| instance_id.as_ref(), |
| &tmp_proxy, |
| ) |
| .await; |
| if let ExpectedResult::Ok = &expected_res { |
| res.expect("failed to read file"); |
| } |
| } else { |
| // Check for the file in the test's isolated test directory |
| let res = capability_util::check_file_in_storage( |
| storage_subdir, |
| relative_moniker, |
| instance_id.as_ref(), |
| &self.test_dir_proxy, |
| ) |
| .await; |
| if let ExpectedResult::Ok = &expected_res { |
| res.expect("failed to read file"); |
| } |
| } |
| } |
| } |
| CheckUse::StorageAdmin { |
| storage_relation, |
| from_cm_namespace, |
| storage_subdir, |
| expected_res, |
| } => { |
| let storage_admin_proxy = |
| capability_util::connect_to_svc_in_namespace::<fsys::StorageAdminMarker>( |
| &namespace, |
| &"/svc/fuchsia.sys2.StorageAdmin".try_into().unwrap(), |
| ) |
| .await; |
| let (storage_proxy, server_end) = create_proxy().unwrap(); |
| let flags = OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE; |
| let relative_moniker_string = format!("{}", storage_relation); |
| let component_abs_moniker = |
| AbsoluteMoniker::from_relative(&moniker, &storage_relation).unwrap(); |
| let component_instance_id = self |
| .model |
| .root |
| .try_get_context() |
| .unwrap() |
| .component_id_index() |
| .look_up_moniker(&component_abs_moniker) |
| .cloned(); |
| storage_admin_proxy |
| .open_component_storage( |
| relative_moniker_string.as_str(), |
| flags, |
| MODE_TYPE_DIRECTORY, |
| server_end, |
| ) |
| .expect("failed to open component storage"); |
| |
| let storage_proxy = |
| DirectoryProxy::from_channel(storage_proxy.into_channel().unwrap()); |
| |
| capability_util::write_hippo_file_to_directory( |
| &storage_proxy, |
| expected_res.clone(), |
| ) |
| .await; |
| if expected_res == ExpectedResult::Ok { |
| let storage_dir = if from_cm_namespace { |
| io_util::open_directory_in_namespace("/tmp", io_util::OPEN_RIGHT_READABLE) |
| .expect("failed to open /tmp") |
| } else { |
| io_util::clone_directory(&self.test_dir_proxy, CLONE_FLAG_SAME_RIGHTS) |
| .expect("failed to clone test_dir_proxy") |
| }; |
| capability_util::check_file_in_storage( |
| storage_subdir.clone(), |
| storage_relation.clone(), |
| component_instance_id.as_ref(), |
| &storage_dir, |
| ) |
| .await |
| .expect("failed to read file"); |
| storage_admin_proxy |
| .delete_component_storage(relative_moniker_string.as_str()) |
| .await |
| .expect("failed to send fidl message") |
| .expect("error encountered while deleting component storage"); |
| capability_util::confirm_storage_is_deleted_for_component( |
| storage_subdir, |
| storage_relation, |
| component_instance_id.as_ref(), |
| &storage_dir, |
| ) |
| .await; |
| } |
| } |
| CheckUse::Event { requests, expected_res } => { |
| // Fails if the component did not use the protocol EventSource or if the event is |
| // not allowed. |
| capability_util::subscribe_to_event_stream(&namespace, expected_res, requests) |
| .await; |
| } |
| } |
| } |
| |
| pub async fn bind_and_get_namespace(&self, moniker: AbsoluteMoniker) -> Arc<ManagedNamespace> { |
| let component_name = self.bind_instance_and_wait_start(&moniker).await.unwrap(); |
| let component_resolved_url = Self::resolved_url(&component_name); |
| let namespace = self |
| .mock_runner |
| .get_namespace(&component_resolved_url) |
| .expect("could not find namespace"); |
| Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await; |
| namespace |
| } |
| |
| /// Checks using a capability from a component's exposed directory. |
| pub async fn check_use_exposed_dir(&self, moniker: AbsoluteMoniker, check: CheckUse) { |
| match check { |
| CheckUse::Protocol { path, expected_res } => { |
| capability_util::call_echo_svc_from_exposed_dir( |
| path, |
| &moniker, |
| &self.model, |
| expected_res, |
| ) |
| .await; |
| } |
| CheckUse::Directory { path, file, expected_res } => { |
| capability_util::read_data_from_exposed_dir( |
| path, |
| &file, |
| &moniker, |
| &self.model, |
| expected_res, |
| ) |
| .await; |
| } |
| CheckUse::Storage { .. } => { |
| panic!("storage capabilities can't be exposed"); |
| } |
| CheckUse::StorageAdmin { .. } => { |
| panic!("unimplemented"); |
| } |
| CheckUse::Event { .. } => { |
| panic!("event capabilities can't be exposed"); |
| } |
| } |
| } |
| |
| /// Lists the contents of a storage directory. |
| pub async fn list_directory_in_storage( |
| &self, |
| subdir: Option<&str>, |
| relation: RelativeMoniker, |
| instance_id: Option<&ComponentInstanceId>, |
| relative_path: &str, |
| ) -> Vec<String> { |
| let dir_path = capability_util::generate_storage_path( |
| subdir.map(|s| s.to_string()), |
| &relation, |
| instance_id, |
| ); |
| let mut dir_path = dir_path.parent().unwrap().to_path_buf(); |
| if !relative_path.is_empty() { |
| dir_path.push(relative_path); |
| } |
| if !dir_path.parent().is_none() { |
| let dir_proxy = io_util::open_directory( |
| &self.test_dir_proxy, |
| &dir_path, |
| io_util::OPEN_RIGHT_READABLE, |
| ) |
| .expect("failed to open directory"); |
| list_directory(&dir_proxy).await |
| } else { |
| list_directory(&self.test_dir_proxy).await |
| } |
| } |
| |
| /// Lists the contents of a directory. |
| pub async fn list_directory(&self, path: &str) -> Vec<String> { |
| let dir_proxy = io_util::open_directory( |
| &self.test_dir_proxy, |
| Path::new(path), |
| io_util::OPEN_RIGHT_READABLE, |
| ) |
| .expect("failed to open directory"); |
| list_directory(&dir_proxy).await |
| } |
| |
| /// check_namespace will ensure that the paths in `namespaces` for `component_name` match the use |
| /// declarations for the the component by the same name in `components`. |
| async fn check_namespace( |
| component_name: String, |
| runner: &MockRunner, |
| components: Vec<(&str, ComponentDecl)>, |
| ) { |
| let (_, decl) = components |
| .into_iter() |
| .find(|(name, _)| name == &component_name) |
| .expect("component not in component decl list"); |
| // Two services installed into the same dir will cause duplicates, so use a HashSet to remove |
| // them. |
| let expected_paths_hs: HashSet<String> = decl |
| .uses |
| .into_iter() |
| .filter_map(|u| match u { |
| UseDecl::Directory(d) => Some(d.target_path.to_string()), |
| UseDecl::Service(s) => Some(s.target_path.to_string()), |
| UseDecl::Protocol(s) => Some(s.target_path.dirname), |
| UseDecl::Storage(s) => Some(s.target_path.to_string()), |
| UseDecl::Runner(_) | UseDecl::Event(_) | UseDecl::EventStream(_) => None, |
| }) |
| .collect(); |
| let mut expected_paths = vec![]; |
| expected_paths.extend(expected_paths_hs.into_iter()); |
| |
| // Get the paths in the component's namespace. |
| let mut actual_paths: Vec<String> = runner |
| .get_namespace(&format!("test:///{}_resolved", component_name)) |
| .expect("component not in namespace") |
| .lock() |
| .await |
| .iter() |
| .map(|entry| entry.path.as_ref().unwrap().clone()) |
| .collect(); |
| |
| expected_paths.sort_unstable(); |
| actual_paths.sort_unstable(); |
| assert_eq!(expected_paths, actual_paths); |
| } |
| |
| /// Checks a `use /svc/fuchsia.sys2.Realm` declaration at `moniker` by calling |
| /// `BindChild`. |
| pub async fn check_use_realm( |
| &self, |
| moniker: AbsoluteMoniker, |
| bind_calls: Arc<Mutex<Vec<String>>>, |
| ) { |
| let component_name = |
| self.bind_instance_and_wait_start(&moniker).await.expect("bind instance failed"); |
| let component_resolved_url = Self::resolved_url(&component_name); |
| let path = "/svc/fuchsia.sys2.Realm".try_into().unwrap(); |
| Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await; |
| capability_util::call_realm_svc( |
| path, |
| &component_resolved_url, |
| &self.mock_runner.get_namespace(&component_resolved_url).unwrap(), |
| bind_calls.clone(), |
| ) |
| .await; |
| } |
| |
| /// Checks that a use declaration of `path` at `moniker` can be opened with |
| /// Fuchsia file operations. |
| pub async fn check_open_file(&self, moniker: AbsoluteMoniker, path: CapabilityPath) { |
| let component_name = |
| self.bind_instance_and_wait_start(&moniker).await.expect("bind instance failed"); |
| let component_resolved_url = Self::resolved_url(&component_name); |
| Self::check_namespace(component_name, &self.mock_runner, self.components.clone()).await; |
| let namespace = self.mock_runner.get_namespace(&component_resolved_url).unwrap(); |
| capability_util::call_file_svc_from_namespace(&namespace, path).await; |
| } |
| |
| /// Build an outgoing directory for the given component. |
| fn build_outgoing_dir( |
| decl: &ComponentDecl, |
| test_dir_proxy: &DirectoryProxy, |
| mut outgoing_paths: HashMap<CapabilityPath, Arc<dyn DirectoryEntry>>, |
| ) -> OutDir { |
| // if this decl is offering/exposing something from `Self`, let's host it |
| let mut out_dir = OutDir::new(); |
| for expose in decl.exposes.iter() { |
| match expose { |
| ExposeDecl::Service(_) => panic!("service capability unsupported"), |
| _ => (), |
| } |
| } |
| for offer in decl.offers.iter() { |
| match offer { |
| OfferDecl::Service(_) => panic!("service capability unsupported"), |
| _ => (), |
| } |
| } |
| for capability in decl.capabilities.iter() { |
| match capability { |
| CapabilityDecl::Protocol(_) => { |
| Self::install_default_out_files(&mut out_dir); |
| } |
| CapabilityDecl::Directory(_) => out_dir.add_directory_proxy(test_dir_proxy), |
| CapabilityDecl::Storage(storage) => { |
| // Storage capabilities can have a source of "self", so there are situations we |
| // want to test where a storage capability is offered and used and there's no |
| // directory capability in the manifest, so we must host the directory structure |
| // for this case in addition to directory offers. |
| if storage.source == StorageDirectorySource::Self_ { |
| out_dir.add_directory_proxy(test_dir_proxy) |
| } |
| } |
| _ => {} |
| } |
| } |
| // Add any user-specific DirectoryEntry objects into the outgoing namespace. |
| for (path, entry) in outgoing_paths.drain() { |
| out_dir.add_entry(path, entry); |
| } |
| |
| out_dir |
| } |
| |
| /// Attempt to bind the instance associated with the given moniker with the |
| /// default reason of BindReason::Eager. |
| /// |
| /// On success, returns the short name of the component. |
| pub async fn bind_instance(&self, moniker: &AbsoluteMoniker) -> Result<String, ModelError> { |
| self.bind_instance_with(moniker, BindReason::Eager, false).await |
| } |
| |
| /// Attempt to bind the instance associated with the given moniker with the |
| /// default reason of BindReason::Eager, and waits for the runner to start the component. |
| /// This method will only work if the component in question is using the default mock runner |
| /// (otherwise, it will hang). |
| /// |
| /// On success, returns the short name of the component. |
| pub async fn bind_instance_and_wait_start( |
| &self, |
| moniker: &AbsoluteMoniker, |
| ) -> Result<String, ModelError> { |
| self.bind_instance_with(moniker, BindReason::Eager, true).await |
| } |
| |
| async fn bind_instance_with( |
| &self, |
| moniker: &AbsoluteMoniker, |
| reason: BindReason, |
| wait_for_start: bool, |
| ) -> Result<String, ModelError> { |
| self.model.bind(moniker, &reason).await?; |
| let component_name = match moniker.path().last() { |
| Some(part) => part.name().to_string(), |
| None => self.root_component_name.to_string(), |
| }; |
| if wait_for_start { |
| let resolved_url = Self::resolved_url(&component_name); |
| self.mock_runner.wait_for_url(&resolved_url).await; |
| } |
| Ok(component_name) |
| } |
| |
| /// Wait for the given component to start running. |
| /// |
| /// We define "running" as "the components outgoing directory has responded to a simple |
| /// request", which the MockRunner supports. |
| pub async fn wait_for_component_start(&self, component: &AbsoluteMoniker) { |
| // Lookup, bind, and open a connection to the component's outgoing directory. |
| let (dir_proxy, server_end) = |
| fidl::endpoints::create_proxy::<fidl_fuchsia_io::DirectoryMarker>().unwrap(); |
| let component = self.model.look_up(component).await.expect("lookup component failed"); |
| let mut server_end = server_end.into_channel(); |
| self.model |
| .bind(&component.abs_moniker, &BindReason::Eager) |
| .await |
| .expect("failed to bind to component") |
| .open_outgoing( |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE, |
| fidl_fuchsia_io::MODE_TYPE_DIRECTORY, |
| PathBuf::from("/."), |
| &mut server_end, |
| ) |
| .await |
| .expect("failed to open component's outgoing directory"); |
| |
| // Ensure we can successfully talk to the directory. |
| dir_proxy.sync().await.expect("could not communicate with directory"); |
| } |
| |
| pub fn resolved_url(component_name: &str) -> String { |
| format!("test:///{}_resolved", component_name) |
| } |
| } |
| |
| /// Installs a new directory at `path` in the test's namespace, removing it when this object |
| /// goes out of scope. |
| pub struct ScopedNamespaceDir<'a> { |
| path: &'a str, |
| } |
| |
| impl<'a> ScopedNamespaceDir<'a> { |
| pub fn new(test: &RoutingTest, path: &'a str) -> Self { |
| let (client_chan, server_chan) = zx::Channel::create().unwrap(); |
| let ns = fdio::Namespace::installed().expect("Failed to get installed namespace"); |
| ns.bind(path, client_chan).expect(&format!("Failed to bind dir {}", path)); |
| let mut out_dir = OutDir::new(); |
| RoutingTest::install_default_out_files(&mut out_dir); |
| out_dir.add_directory_proxy(&test.test_dir_proxy); |
| out_dir.host_fn()(ServerEnd::new(server_chan)); |
| Self { path } |
| } |
| } |
| |
| impl Drop for ScopedNamespaceDir<'_> { |
| fn drop(&mut self) { |
| let ns = fdio::Namespace::installed().expect("Failed to get installed namespace"); |
| ns.unbind(self.path).expect(&format!("Failed to unbind dir {}", self.path)); |
| } |
| } |
| |
| /// Contains functions to use capabilities in routing tests. |
| pub mod capability_util { |
| use { |
| super::*, crate::model::component_id_index::ComponentInstanceId, anyhow::format_err, |
| cm_rust::NativeIntoFidl, fidl::endpoints::ServiceMarker, |
| fidl_fuchsia_sys2::BlockingEventSourceMarker, std::path::PathBuf, |
| }; |
| |
| /// Looks up `resolved_url` in the namespace, and attempts to read ${path}/hippo. The file |
| /// should contain the string "hello". |
| pub async fn read_data_from_namespace( |
| namespace: &ManagedNamespace, |
| path: CapabilityPath, |
| file: &Path, |
| expected_res: ExpectedResult, |
| ) { |
| let path = path.to_string(); |
| let dir_proxy = take_dir_from_namespace(namespace, &path).await; |
| let file_proxy = |
| io_util::open_file(&dir_proxy, file, OPEN_RIGHT_READABLE).expect("failed to open file"); |
| let res = io_util::read_file(&file_proxy).await; |
| match expected_res { |
| ExpectedResult::Ok => assert_eq!( |
| "hello", |
| res.expect(&format!("failed to read file {}", path.to_string())) |
| ), |
| ExpectedResult::Err(s) => { |
| assert!(res.is_err(), "read file successfully when it should fail"); |
| let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph"); |
| assert_matches!( |
| epitaph, |
| Err(fidl::Error::ClientChannelClosed { |
| status, .. |
| }) if status == s |
| ); |
| } |
| ExpectedResult::ErrWithNoEpitaph => { |
| assert!(res.is_err(), "read file successfully when it should fail"); |
| assert_matches!(dir_proxy.take_event_stream().next().await, None); |
| } |
| } |
| // We took ownership of `dir_proxy`, add it back to the namespace. |
| add_dir_to_namespace(namespace, &path, dir_proxy).await; |
| } |
| |
| pub async fn write_file_to_storage( |
| namespace: &ManagedNamespace, |
| path: CapabilityPath, |
| expected_res: ExpectedResult, |
| ) { |
| let dir_path = path.to_string(); |
| let dir_proxy = take_dir_from_namespace(namespace, dir_path.as_str()).await; |
| write_hippo_file_to_directory(&dir_proxy, expected_res).await; |
| add_dir_to_namespace(namespace, dir_path.as_str(), dir_proxy).await; |
| } |
| |
| pub async fn write_hippo_file_to_directory( |
| dir_proxy: &DirectoryProxy, |
| expected_res: ExpectedResult, |
| ) { |
| let (file_proxy, server_end) = create_proxy::<FileMarker>().unwrap(); |
| let flags = OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE; |
| let res = async { |
| dir_proxy.open( |
| flags, |
| MODE_TYPE_FILE, |
| "hippos", |
| ServerEnd::new(server_end.into_channel()), |
| )?; |
| file_proxy.write(b"hippos can be stored here").await |
| } |
| .await; |
| match expected_res { |
| ExpectedResult::Ok => { |
| let (s, _) = res.expect("failed to write file"); |
| assert_matches!(zx::Status::from_raw(s), zx::Status::OK); |
| } |
| ExpectedResult::Err(s) => { |
| res.expect_err("unexpectedly succeeded writing file"); |
| let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph"); |
| assert_matches!( |
| epitaph, |
| Err(fidl::Error::ClientChannelClosed { |
| status, .. |
| }) if status == s |
| ); |
| } |
| ExpectedResult::ErrWithNoEpitaph => { |
| res.expect_err("unexpectedly succeeded writing file"); |
| assert_matches!(dir_proxy.take_event_stream().next().await, None); |
| } |
| } |
| } |
| |
| /// Create a file with the given contents in the test dir, along with any subdirectories |
| /// required. |
| pub(super) async fn create_static_file( |
| root: &DirectoryProxy, |
| path: &Path, |
| contents: &str, |
| ) -> Result<(), anyhow::Error> { |
| // Open file, and create subdirectories if required. |
| let file_proxy = if let Some(directory) = path.parent() { |
| let subdir = io_util::create_sub_directories(root, directory) |
| .map_err(|e| e.context(format!("failed to create subdirs for {:?}", path)))?; |
| io_util::open_file( |
| &subdir, |
| &PathBuf::from(path.file_name().unwrap()), |
| OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE, |
| )? |
| } else { |
| io_util::open_file(root, path, OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE)? |
| }; |
| |
| // Write contents. |
| io_util::write_file(&file_proxy, contents).await |
| } |
| |
| pub async fn check_file_in_storage( |
| storage_subdir: Option<String>, |
| relation: RelativeMoniker, |
| instance_id: Option<&ComponentInstanceId>, |
| test_dir_proxy: &DirectoryProxy, |
| ) -> Result<(), anyhow::Error> { |
| let mut dir_path = generate_storage_path(storage_subdir, &relation, instance_id); |
| dir_path.push("hippos"); |
| let file_proxy = |
| io_util::open_file(&test_dir_proxy, &dir_path, io_util::OPEN_RIGHT_READABLE)?; |
| let res = io_util::read_file(&file_proxy).await; |
| |
| if let Ok(contents) = res { |
| assert_eq!("hippos can be stored here".to_string(), contents); |
| Ok(()) |
| } else { |
| Err(res.expect_err("failed to read file")) |
| } |
| } |
| |
| pub async fn confirm_storage_is_deleted_for_component( |
| storage_subdir: Option<String>, |
| relation: RelativeMoniker, |
| instance_id: Option<&ComponentInstanceId>, |
| test_dir_proxy: &DirectoryProxy, |
| ) { |
| let dir_path = generate_storage_path(storage_subdir, &relation, instance_id); |
| let res = io_util::directory::open_directory( |
| &test_dir_proxy, |
| dir_path.to_str().unwrap(), |
| io_util::OPEN_RIGHT_READABLE, |
| ) |
| .await |
| .expect_err("open_directory shouldnt have succeeded"); |
| assert_eq!( |
| format!("{:?}", res), |
| format!("{:?}", io_util::node::OpenError::OpenError(zx::Status::NOT_FOUND)) |
| ); |
| } |
| |
| pub async fn connect_to_svc_in_namespace<T: ServiceMarker>( |
| namespace: &ManagedNamespace, |
| path: &CapabilityPath, |
| ) -> T::Proxy { |
| let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await; |
| let node_proxy = io_util::open_node( |
| &dir_proxy, |
| &Path::new(&path.basename), |
| OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, |
| MODE_TYPE_SERVICE, |
| ) |
| .expect("failed to open echo service"); |
| add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await; |
| let client_end = ClientEnd::<T>::new(node_proxy.into_channel().unwrap().into_zx_channel()); |
| client_end.into_proxy().unwrap() |
| } |
| |
| /// Verifies that it's possible to subscribe to the given `events` by connecting to an |
| /// `BlockingEventSource` on the given `namespace`. Used to test eventcapability routing. |
| /// Testing of usage of the stream lives in the integration tests in: |
| /// //src/sys/component_manager/tests/events/integration_test.rs |
| pub async fn subscribe_to_event_stream( |
| namespace: &ManagedNamespace, |
| expected_res: ExpectedResult, |
| events: Vec<EventSubscription>, |
| ) { |
| let path: CapabilityPath = "/svc/fuchsia.sys2.BlockingEventSource".parse().unwrap(); |
| let res = subscribe_to_events(namespace, &path, events).await; |
| match (res, expected_res) { |
| (Err(e), ExpectedResult::Ok) => { |
| panic!("unexpected failure {}", e); |
| } |
| (Ok(_), ExpectedResult::Err(_)) | (Ok(_), ExpectedResult::ErrWithNoEpitaph) => { |
| panic!("unexpected success"); |
| } |
| _ => {} |
| } |
| } |
| |
| pub async fn subscribe_to_events( |
| namespace: &ManagedNamespace, |
| event_source_path: &CapabilityPath, |
| events: Vec<EventSubscription>, |
| ) -> Result<fsys::EventStreamRequestStream, anyhow::Error> { |
| let event_source_proxy = |
| connect_to_svc_in_namespace::<BlockingEventSourceMarker>(namespace, event_source_path) |
| .await; |
| let (client_end, stream) = |
| fidl::endpoints::create_request_stream::<fsys::EventStreamMarker>()?; |
| event_source_proxy |
| .subscribe( |
| &mut events.into_iter().map(|request| fsys::EventSubscription { |
| event_name: Some(request.event_name.to_string()), |
| mode: Some(match request.mode { |
| EventMode::Sync => fsys::EventMode::Sync, |
| _ => fsys::EventMode::Async, |
| }), |
| ..fsys::EventSubscription::EMPTY |
| }), |
| client_end, |
| ) |
| .await? |
| .map_err(|error| format_err!("Unable to subscribe to event stream: {:?}", error))?; |
| event_source_proxy.start_component_tree().await?; |
| Ok(stream) |
| } |
| |
| /// Looks up `resolved_url` in the namespace, and attempts to use `path`. Expects the service |
| /// to be fidl.examples.echo.Echo. |
| pub async fn call_echo_svc_from_namespace( |
| namespace: &ManagedNamespace, |
| path: CapabilityPath, |
| expected_res: ExpectedResult, |
| ) { |
| let echo_proxy = connect_to_svc_in_namespace::<echo::EchoMarker>(namespace, &path).await; |
| let res = echo_proxy.echo_string(Some("hippos")).await; |
| |
| match expected_res { |
| ExpectedResult::Ok => { |
| assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string())) |
| } |
| ExpectedResult::Err(s) => { |
| let err = res.expect_err("used echo service successfully when it should fail"); |
| assert!(err.is_closed(), "expected file closed error, got: {:?}", err); |
| let epitaph = echo_proxy.take_event_stream().next().await.expect("no epitaph"); |
| assert_matches!( |
| epitaph, |
| Err(fidl::Error::ClientChannelClosed { |
| status, .. |
| }) if status == s |
| ); |
| } |
| ExpectedResult::ErrWithNoEpitaph => { |
| let err = res.expect_err("used echo service successfully when it should fail"); |
| assert!(err.is_closed(), "expected file closed error, got: {:?}", err); |
| assert_matches!(echo_proxy.take_event_stream().next().await, None); |
| } |
| } |
| } |
| |
| /// Looks up `resolved_url` in the namespace, and attempts to use `path`. |
| /// Expects the service to work like a fuchsia.io service, and respond with |
| /// an OnOpen event when opened with OPEN_FLAG_DESCRIBE. |
| pub async fn call_file_svc_from_namespace(namespace: &ManagedNamespace, path: CapabilityPath) { |
| let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await; |
| let node_proxy = io_util::open_node( |
| &dir_proxy, |
| &Path::new(&path.basename), |
| OPEN_RIGHT_READABLE | OPEN_FLAG_DESCRIBE, |
| // This should be MODE_TYPE_SERVICE, but we implement the underlying |
| // service as a file for convenience in testing. |
| MODE_TYPE_FILE, |
| ) |
| .expect("failed to open file service"); |
| add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await; |
| |
| let file_proxy = FileProxy::new(node_proxy.into_channel().unwrap()); |
| let mut event_stream = file_proxy.take_event_stream(); |
| let event = event_stream.try_next().await.unwrap(); |
| let FileEvent::OnOpen_ { s, info } = event.expect("failed to received file event"); |
| assert_eq!(s, zx::sys::ZX_OK); |
| assert_eq!( |
| *info.expect("failed to receive node info"), |
| NodeInfo::File(FileObject { event: None, stream: None }) |
| ); |
| } |
| |
| /// Attempts to read ${path}/hippo in `abs_moniker`'s exposed directory. The file should |
| /// contain the string "hello". |
| pub async fn read_data_from_exposed_dir<'a>( |
| path: CapabilityPath, |
| file: &Path, |
| abs_moniker: &'a AbsoluteMoniker, |
| model: &'a Arc<Model>, |
| expected_res: ExpectedResult, |
| ) { |
| let (node_proxy, server_end) = endpoints::create_proxy::<NodeMarker>().unwrap(); |
| open_exposed_dir(&path, abs_moniker, model, MODE_TYPE_DIRECTORY, server_end).await; |
| let dir_proxy = DirectoryProxy::new(node_proxy.into_channel().unwrap()); |
| match expected_res { |
| ExpectedResult::Ok => { |
| let file_proxy = io_util::open_file(&dir_proxy, &file, OPEN_RIGHT_READABLE) |
| .expect("failed to open file"); |
| let res = io_util::read_file(&file_proxy).await; |
| assert_eq!("hello", res.expect("failed to read file")); |
| } |
| ExpectedResult::Err(s) => { |
| io_util::open_file(&dir_proxy, &file, OPEN_RIGHT_READABLE) |
| .expect_err("opened file successfully when it should fail"); |
| let epitaph = dir_proxy.take_event_stream().next().await.expect("no epitaph"); |
| assert_matches!( |
| epitaph, |
| Err(fidl::Error::ClientChannelClosed { |
| status, .. |
| }) if status == s |
| ); |
| } |
| ExpectedResult::ErrWithNoEpitaph => { |
| io_util::open_file(&dir_proxy, &file, OPEN_RIGHT_READABLE) |
| .expect_err("opened file successfully when it should fail"); |
| assert_matches!(dir_proxy.take_event_stream().next().await, None); |
| } |
| } |
| } |
| |
| /// Attempts to use the fidl.examples.echo.Echo service at `path` in `abs_moniker`'s exposed |
| /// directory. |
| pub async fn call_echo_svc_from_exposed_dir<'a>( |
| path: CapabilityPath, |
| abs_moniker: &'a AbsoluteMoniker, |
| model: &'a Arc<Model>, |
| expected_res: ExpectedResult, |
| ) { |
| let (node_proxy, server_end) = endpoints::create_proxy::<NodeMarker>().unwrap(); |
| open_exposed_dir(&path, abs_moniker, model, MODE_TYPE_SERVICE, server_end).await; |
| let echo_proxy = echo::EchoProxy::new(node_proxy.into_channel().unwrap()); |
| let res = echo_proxy.echo_string(Some("hippos")).await; |
| match expected_res { |
| ExpectedResult::Ok => { |
| assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string())) |
| } |
| ExpectedResult::Err(s) => { |
| let err = res.expect_err("used echo service successfully when it should fail"); |
| assert!(err.is_closed(), "expected file closed error, got: {:?}", err); |
| let epitaph = echo_proxy.take_event_stream().next().await.expect("no epitaph"); |
| assert_matches!( |
| epitaph, |
| Err(fidl::Error::ClientChannelClosed { |
| status, .. |
| }) if status == s |
| ); |
| } |
| ExpectedResult::ErrWithNoEpitaph => { |
| let err = res.expect_err("used echo service successfully when it should fail"); |
| assert!(err.is_closed(), "expected file closed error, got: {:?}", err); |
| assert_matches!(echo_proxy.take_event_stream().next().await, None); |
| } |
| } |
| } |
| |
| /// Looks up `resolved_url` in the namespace, and attempts to use `path`. Expects the service |
| /// to be fuchsia.sys2.Realm. |
| pub async fn call_realm_svc( |
| path: CapabilityPath, |
| resolved_url: &str, |
| namespace: &ManagedNamespace, |
| bind_calls: Arc<Mutex<Vec<String>>>, |
| ) { |
| let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await; |
| let node_proxy = io_util::open_node( |
| &dir_proxy, |
| &Path::new(&path.basename), |
| OPEN_RIGHT_READABLE, |
| MODE_TYPE_SERVICE, |
| ) |
| .expect("failed to open realm service"); |
| let realm_proxy = fsys::RealmProxy::new(node_proxy.into_channel().unwrap()); |
| let mut child_ref = fsys::ChildRef { name: "my_child".to_string(), collection: None }; |
| let (_client_chan, server_chan) = zx::Channel::create().unwrap(); |
| let exposed_capabilities = ServerEnd::new(server_chan); |
| let res = realm_proxy.bind_child(&mut child_ref, exposed_capabilities).await; |
| |
| // Check for side effects: realm service should have received the `bind_child` call. |
| let _ = res.expect("failed to use realm service"); |
| let bind_url = |
| format!("test:///{}_resolved", bind_calls.lock().await.last().expect("no bind call")); |
| assert_eq!(bind_url, resolved_url); |
| } |
| |
| /// Call `fuchsia.sys2.Realm.CreateChild` to create a dynamic child. |
| pub async fn call_create_child<'a>( |
| namespace: &ManagedNamespace, |
| collection: &'a str, |
| child_decl: ChildDecl, |
| ) { |
| let path: CapabilityPath = "/svc/fuchsia.sys2.Realm".try_into().expect("no realm service"); |
| let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await; |
| let node_proxy = io_util::open_node( |
| &dir_proxy, |
| &Path::new(&path.basename), |
| OPEN_RIGHT_READABLE, |
| MODE_TYPE_SERVICE, |
| ) |
| .expect("failed to open realm service"); |
| add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await; |
| let realm_proxy = fsys::RealmProxy::new(node_proxy.into_channel().unwrap()); |
| let mut collection_ref = fsys::CollectionRef { name: collection.to_string() }; |
| let child_decl = child_decl.native_into_fidl(); |
| let res = realm_proxy.create_child(&mut collection_ref, child_decl).await; |
| let _ = res.expect("failed to create child"); |
| } |
| |
| /// Call `fuchsia.sys2.Realm.DestroyChild` to destroy a dynamic child, waiting for |
| /// destruction to complete. |
| pub async fn call_destroy_child<'a>( |
| namespace: &ManagedNamespace, |
| collection: &'a str, |
| name: &'a str, |
| ) { |
| let path: CapabilityPath = "/svc/fuchsia.sys2.Realm".try_into().expect("no realm service"); |
| let dir_proxy = take_dir_from_namespace(namespace, &path.dirname).await; |
| let node_proxy = io_util::open_node( |
| &dir_proxy, |
| &Path::new(&path.basename), |
| OPEN_RIGHT_READABLE, |
| MODE_TYPE_SERVICE, |
| ) |
| .expect("failed to open realm service"); |
| add_dir_to_namespace(namespace, &path.dirname, dir_proxy).await; |
| let realm_proxy = fsys::RealmProxy::new(node_proxy.into_channel().unwrap()); |
| let mut child_ref = |
| fsys::ChildRef { collection: Some(collection.to_string()), name: name.to_string() }; |
| let res = realm_proxy.destroy_child(&mut child_ref).await; |
| let _ = res.expect("failed to destroy child"); |
| } |
| |
| pub async fn take_dir_from_namespace( |
| namespace: &ManagedNamespace, |
| dir_path: &str, |
| ) -> DirectoryProxy { |
| let mut ns = namespace.lock().await; |
| |
| // Find the index of our directory in the namespace, and remove the directory and path. The |
| // path is removed so that the paths/dirs aren't shuffled in the namespace. |
| let index = ns |
| .iter() |
| .position(|entry| entry.path.as_ref().unwrap() == dir_path) |
| .expect(&format!("didn't find dir {}", dir_path)); |
| let entry = ns.remove(index); |
| let dir_proxy = entry.directory.unwrap().into_proxy().unwrap(); |
| dir_proxy |
| } |
| |
| /// Adds `dir_proxy` back to the namespace. Useful for restoring the namespace after a call |
| /// to `take_dir_from_namespace`. |
| pub async fn add_dir_to_namespace( |
| namespace: &ManagedNamespace, |
| dir_path: &str, |
| dir_proxy: DirectoryProxy, |
| ) { |
| let mut ns = namespace.lock().await; |
| ns.push(fcrunner::ComponentNamespaceEntry { |
| path: Some(dir_path.to_string()), |
| directory: Some(ClientEnd::new(dir_proxy.into_channel().unwrap().into_zx_channel())), |
| ..fcrunner::ComponentNamespaceEntry::EMPTY |
| }); |
| } |
| |
| /// Open the exposed dir for `abs_moniker`. |
| async fn open_exposed_dir<'a>( |
| path: &'a CapabilityPath, |
| abs_moniker: &'a AbsoluteMoniker, |
| model: &'a Arc<Model>, |
| open_mode: u32, |
| server_end: ServerEnd<NodeMarker>, |
| ) { |
| let component = model |
| .look_up(abs_moniker) |
| .await |
| .expect(&format!("component not found {}", abs_moniker)); |
| model.bind(abs_moniker, &BindReason::Eager).await.expect("failed to bind instance"); |
| let execution = component.lock_execution().await; |
| let runtime = execution.runtime.as_ref().expect("not resolved"); |
| let flags = OPEN_RIGHT_READABLE; |
| |
| let vns_path = to_fvfs_path(path); |
| runtime.exposed_dir.open(flags, open_mode, vns_path, server_end); |
| } |
| |
| // This function should reproduce the logic of `crate::storage::generate_storage_path` |
| pub fn generate_storage_path( |
| subdir: Option<String>, |
| relative_moniker: &RelativeMoniker, |
| instance_id: Option<&ComponentInstanceId>, |
| ) -> PathBuf { |
| if let Some(id) = instance_id { |
| return [id].iter().collect(); |
| } |
| assert!(relative_moniker.up_path().is_empty()); |
| let mut down_path = relative_moniker.down_path().iter(); |
| let mut dir_path = vec![]; |
| if let Some(subdir) = subdir { |
| dir_path.push(subdir); |
| } |
| if let Some(p) = down_path.next() { |
| dir_path.push(p.as_str().to_string()); |
| } |
| while let Some(p) = down_path.next() { |
| dir_path.push("children".to_string()); |
| dir_path.push(p.as_str().to_string()); |
| } |
| |
| // Storage capabilities used to have a hardcoded set of types, which would be appended |
| // here. To maintain compatibility with the old paths (and thus not lose data when this was |
| // migrated) we append "data" here. This works because this is the only type of storage |
| // that was actually used in the wild. |
| // |
| // This is only temporary, until the storage instance id migration changes this layout. |
| dir_path.push("data".to_string()); |
| dir_path.into_iter().collect() |
| } |
| |
| /// Function to convert a CapabilityPath to a pseudo_fs_mt::Path |
| fn to_fvfs_path(path: &CapabilityPath) -> vfs::path::Path { |
| let full_path = format!("{}/{}", path.dirname, path.basename); |
| let split_string = full_path.split('/').filter(|s| !s.is_empty()).collect::<Vec<_>>(); |
| vfs::path::Path::validate_and_split(split_string.join("/")) |
| .expect("Failed to validate and split path") |
| } |
| } |