blob: f1421ce41bd58dbcd207d333eabc6c70990c0183 [file] [log] [blame]
// 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_environment::BuiltinEnvironment,
klog,
model::{
binding::Binder,
error::ModelError,
hooks::HooksRegistration,
model::{ComponentManagerConfig, Model, ModelParams},
moniker::{AbsoluteMoniker, PartialMoniker, RelativeMoniker},
resolver::ResolverRegistry,
runner::Runner,
testing::{echo_service::*, mocks::*, out_dir::OutDir, test_helpers::*},
},
startup,
},
cm_rust::*,
fidl::endpoints::{self, create_proxy, ClientEnd, 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,
fuchsia_zircon::HandleBased,
futures::lock::Mutex,
futures::prelude::*,
std::{
collections::{HashMap, HashSet},
convert::{TryFrom, TryInto},
default::Default,
ffi::CString,
path::{Path, PathBuf},
ptr,
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()
}
pub enum CheckUse {
Protocol {
path: CapabilityPath,
should_succeed: bool,
},
Directory {
path: CapabilityPath,
file: PathBuf,
should_succeed: bool,
},
Storage {
path: CapabilityPath,
type_: fsys::StorageType,
// The relative moniker from the storage declaration to the use declaration. If None, this
// storage use is expected to fail.
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,
},
Event {
names: Vec<CapabilityName>,
should_be_allowed: bool,
},
}
impl CheckUse {
pub fn default_directory(should_succeed: bool) -> Self {
Self::Directory {
path: default_directory_capability(),
file: PathBuf::from("hippo"),
should_succeed,
}
}
}
/// 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 Runner>>,
// 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>,
}
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", offered to the root component
/// under the given name.
pub fn add_builtin_runner(
mut self,
name: impl Into<CapabilityName>,
runner: Arc<dyn Runner>,
) -> 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 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,
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 mut resolver = ResolverRegistry::new();
let 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"), "hippo")
.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(),
};
runner.add_host_fn(&format!("test:///{}_resolved", name), host_fn);
mock_resolver.add_component(name, decl.clone());
}
resolver.register("test".to_string(), Box::new(mock_resolver));
// Set up runners for the system, including a default runner "test_runner"
// backed by mock_runner.
let mut builtin_runners = builder.builtin_runners;
builtin_runners.insert(TEST_RUNNER_NAME.into(), runner.clone());
let startup_args = startup::Arguments {
use_builtin_process_launcher: false,
root_component_url: "".to_string(),
debug: false,
};
let echo_service = Arc::new(EchoService::new());
let model = Arc::new(Model::new(ModelParams {
root_component_url: format!("test:///{}", builder.root_component),
root_resolver_registry: resolver,
}));
let builtin_environment = BuiltinEnvironment::new(
&startup_args,
&model,
ComponentManagerConfig::default(),
&builtin_runners,
)
.await
.expect("builtin environment setup failed");
model.root_realm.hooks.install(builder.additional_hooks).await;
model.root_realm.hooks.install(echo_service.hooks()).await;
Self {
components: builder.components,
model,
builtin_environment,
_echo_service: echo_service,
mock_runner: runner,
_test_dir: test_dir,
test_dir_proxy,
root_component_name: builder.root_component,
}
}
/// Installs a new directory at /hippo in our namespace. Does nothing if this directory already
/// exists.
pub fn install_hippo_dir(&self, path: &str) {
let (client_chan, server_chan) = zx::Channel::create().unwrap();
let mut ns_ptr: *mut fdio::fdio_sys::fdio_ns_t = ptr::null_mut();
let status = unsafe { fdio::fdio_sys::fdio_ns_get_installed(&mut ns_ptr) };
if status != zx::sys::ZX_OK {
panic!(
"bad status returned for fdio_ns_get_installed: {}",
zx::Status::from_raw(status)
);
}
let cstr = CString::new(path).unwrap();
let status =
unsafe { fdio::fdio_sys::fdio_ns_bind(ns_ptr, cstr.as_ptr(), client_chan.into_raw()) };
if status != zx::sys::ZX_OK && status != zx::sys::ZX_ERR_ALREADY_EXISTS {
panic!("bad status returned for fdio_ns_bind: {}", zx::Status::from_raw(status));
}
let mut out_dir = OutDir::new();
Self::install_default_out_files(&mut out_dir);
out_dir.add_directory_proxy(&self.test_dir_proxy);
out_dir.host_fn()(ServerEnd::new(server_chan));
}
/// 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.
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(&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 realm = self.model.look_up_realm(&moniker).await.expect("failed to look up realm");
self.model.bind(&realm.abs_moniker).await.expect("bind instance failed");
let partial_moniker = PartialMoniker::new(name.to_string(), Some(collection.to_string()));
let nf =
realm.remove_dynamic_child(&partial_moniker).await.expect("failed to remove child");
// Wait for destruction to fully complete.
nf.await.expect("failed to destroy child");
}
/// 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(&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, should_succeed } => {
capability_util::call_echo_svc_from_namespace(&namespace, path, should_succeed)
.await;
}
CheckUse::Directory { path, file, should_succeed } => {
capability_util::read_data_from_namespace(&namespace, path, &file, should_succeed)
.await
}
CheckUse::Storage { type_: fsys::StorageType::Meta, storage_relation, .. } => {
capability_util::write_file_to_meta_storage(
&self.model,
moniker,
storage_relation.is_some(),
)
.await;
if let Some(relative_moniker) = storage_relation {
capability_util::check_file_in_storage(
fsys::StorageType::Meta,
relative_moniker,
&self.test_dir_proxy,
)
.await;
}
}
CheckUse::Storage { path, type_, storage_relation, from_cm_namespace } => {
capability_util::write_file_to_storage(
&namespace,
path,
storage_relation.is_some(),
)
.await;
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");
capability_util::check_file_in_storage(type_, relative_moniker, &tmp_proxy)
.await;
} else {
// Check for the file in the test's isolated test directory
capability_util::check_file_in_storage(
type_,
relative_moniker,
&self.test_dir_proxy,
)
.await;
}
}
}
CheckUse::Event { names, should_be_allowed } => {
// Fails if the component did not use the protocol EventSource or if the event is
// not allowed.
capability_util::subscribe_to_event_stream(&namespace, should_be_allowed, names)
.await;
}
}
}
/// 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, should_succeed } => {
capability_util::call_echo_svc_from_exposed_dir(
path,
&moniker,
&self.model,
should_succeed,
)
.await;
}
CheckUse::Directory { path, file, should_succeed } => {
capability_util::read_data_from_exposed_dir(
path,
&file,
&moniker,
&self.model,
should_succeed,
)
.await;
}
CheckUse::Storage { .. } => {
panic!("storage capabilities can't be exposed");
}
CheckUse::Event { .. } => {
panic!("event capabilities can't be exposed");
}
}
}
/// Lists the contents of a storage directory.
pub async fn list_directory_in_storage(
&self,
relation: RelativeMoniker,
relative_path: &str,
) -> Vec<String> {
let mut dir_path = capability_util::generate_storage_path(None, &relation);
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
}
}
/// 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(UseStorageDecl::Data(p)) => Some(p.to_string()),
UseDecl::Storage(UseStorageDecl::Cache(p)) => Some(p.to_string()),
UseDecl::Storage(UseStorageDecl::Meta) => None,
UseDecl::Runner(_) | UseDecl::Event(_) => 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(&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(&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"),
ExposeDecl::Protocol(s) if s.source == ExposeSource::Self_ => {
Self::install_default_out_files(&mut out_dir);
}
ExposeDecl::Directory(d) if d.source == ExposeSource::Self_ => {
out_dir.add_directory_proxy(test_dir_proxy)
}
_ => (),
}
}
for offer in decl.offers.iter() {
match offer {
OfferDecl::Service(_) => panic!("service capability unsupported"),
OfferDecl::Protocol(s) if s.source == OfferServiceSource::Self_ => {
Self::install_default_out_files(&mut out_dir);
}
OfferDecl::Directory(d) if d.source == OfferDirectorySource::Self_ => {
out_dir.add_directory_proxy(test_dir_proxy)
}
_ => (),
}
}
for storage in decl.storage.iter() {
// 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
}
/// Atempt to bind the instance associated with the given moniker.
///
/// On success, returns the short name of the component.
pub async fn bind_instance(&self, moniker: &AbsoluteMoniker) -> Result<String, anyhow::Error> {
self.model.bind(moniker).await?;
Ok(match moniker.path().last() {
Some(part) => part.name().to_string(),
None => self.root_component_name.to_string(),
})
}
/// 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 realm's outgoing directory.
let (dir_proxy, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_io::DirectoryMarker>().unwrap();
let realm = self.model.look_up_realm(component).await.expect("lookup root realm failed");
self.model
.bind(&realm.abs_moniker)
.await
.expect("failed to bind to realm")
.open_outgoing(
fidl_fuchsia_io::OPEN_RIGHT_READABLE,
fidl_fuchsia_io::MODE_TYPE_DIRECTORY,
PathBuf::from("/."),
server_end.into_channel(),
)
.await
.expect("failed to open realm'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)
}
}
/// Contains functions to use capabilities in routing tests.
pub mod capability_util {
use {
super::*, crate::model::events::source_factory::EVENT_SOURCE_SYNC_SERVICE_PATH,
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 "hippo".
pub async fn read_data_from_namespace(
namespace: &ManagedNamespace,
path: CapabilityPath,
file: &Path,
should_succeed: bool,
) {
let path = path.to_string();
let dir_proxy = get_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 should_succeed {
true => assert_eq!("hippo", res.expect("failed to read file")),
false => assert!(res.is_err(), "read file successfully when it should fail"),
}
}
pub async fn write_file_to_storage(
namespace: &ManagedNamespace,
path: CapabilityPath,
should_succeed: bool,
) {
let dir_string = path.to_string();
let dir_proxy = get_dir_from_namespace(namespace, dir_string.as_str()).await;
write_hippo_file_to_directory(&dir_proxy, should_succeed).await;
}
pub async fn write_file_to_meta_storage(
model: &Arc<Model>,
moniker: AbsoluteMoniker,
should_succeed: bool,
) {
let realm = model.look_up_realm(&moniker).await.expect("failed to look up realm");
let meta_dir_res = realm.resolve_meta_dir().await;
match (meta_dir_res, should_succeed) {
(Ok(Some(meta_dir)), true) => write_hippo_file_to_directory(&meta_dir, true).await,
(Err(ModelError::CapabilityDiscoveryError { .. }), false) => (),
(Err(ModelError::StorageError { .. }), false) => (),
(Ok(Some(_)), false) => panic!("meta dir present when usage was expected to fail"),
(Ok(None), true) => panic!("meta dir missing when usage was expected to succeed"),
(Ok(None), false) => panic!("meta dir missing when resolution should return an error"),
(Err(_), true) => panic!("meta dir resolution failed usage was expected to succeed"),
(Err(_), false) => panic!("unexpected error when attempting meta dir resolution"),
}
}
async fn write_hippo_file_to_directory(dir_proxy: &DirectoryProxy, should_succeed: bool) {
let (file_proxy, server_end) = create_proxy::<FileMarker>().unwrap();
let flags = OPEN_RIGHT_WRITABLE | OPEN_FLAG_CREATE;
dir_proxy
.open(flags, MODE_TYPE_FILE, "hippos", ServerEnd::new(server_end.into_channel()))
.expect("failed to open file on storage");
let res = file_proxy.write(b"hippos can be stored here").await;
match (res, should_succeed) {
(Ok((s, _)),true) => assert_eq!(zx::Status::OK, zx::Status::from_raw(s)),
(Err(_),false) => (),
(Ok((s, _)),false) => panic!("we shouldn't be able to access storage, but we opened a file! failed write with status {}", zx::Status::from_raw(s)),
(Err(e),true) => panic!("failed to write to file when we expected to be able to! {:?}", e),
}
}
/// 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(
type_: fsys::StorageType,
relation: RelativeMoniker,
test_dir_proxy: &DirectoryProxy,
) {
let mut dir_path = generate_storage_path(Some(type_), &relation);
dir_path.push("hippos");
let file_proxy =
io_util::open_file(&test_dir_proxy, &dir_path, io_util::OPEN_RIGHT_READABLE)
.expect("failed to open file");
let res = io_util::read_file(&file_proxy).await;
assert_eq!("hippos can be stored here".to_string(), res.expect("failed to read file"));
}
async fn connect_to_svc_in_namespace<T: ServiceMarker>(
namespace: &ManagedNamespace,
path: &CapabilityPath,
) -> T::Proxy {
let dir_proxy = get_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 echo service");
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,
should_succeed: bool,
events: Vec<CapabilityName>,
) {
let event_source_proxy = connect_to_svc_in_namespace::<BlockingEventSourceMarker>(
namespace,
&*EVENT_SOURCE_SYNC_SERVICE_PATH,
)
.await;
let event_names: Vec<String> = events.into_iter().map(|event| event.to_string()).collect();
let (client_end, _stream) =
fidl::endpoints::create_request_stream::<fsys::EventStreamMarker>()
.expect("create client");
let res = event_source_proxy
.subscribe(&mut event_names.iter().map(|e| e.as_ref()), client_end)
.await
.expect("failed to use service");
event_source_proxy.start_component_tree().await.expect("failed to start component tree");
assert_eq!(res.is_ok(), should_succeed);
}
/// 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,
should_succeed: bool,
) {
let echo_proxy = connect_to_svc_in_namespace::<echo::EchoMarker>(namespace, &path).await;
let res = echo_proxy.echo_string(Some("hippos")).await;
match should_succeed {
true => {
assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string()))
}
false => {
let err = res.expect_err("used echo service successfully when it should fail");
assert!(err.is_closed(), "expected file closed error, got: {:?}", err);
}
}
}
/// 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 = get_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");
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 "hippo".
pub async fn read_data_from_exposed_dir<'a>(
path: CapabilityPath,
file: &Path,
abs_moniker: &'a AbsoluteMoniker,
model: &'a Arc<Model>,
should_succeed: bool,
) {
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());
let file_proxy = io_util::open_file(&dir_proxy, &file, OPEN_RIGHT_READABLE);
match file_proxy {
Ok(fp) => {
let res = io_util::read_file(&fp).await;
match should_succeed {
true => assert_eq!("hippo", res.expect("failed to read file")),
false => assert!(res.is_err(), "read file successfully when it should fail"),
}
}
Err(_) => {
assert!(!should_succeed, file_proxy.expect("failed to open file when it should"))
}
}
}
/// 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>,
should_succeed: bool,
) {
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 should_succeed {
true => {
assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string()))
}
false => {
let err = res.expect_err("used echo service successfully when it should fail");
assert!(err.is_closed(), "expected file closed error, got: {:?}", err);
}
}
}
/// 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 = get_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 = get_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 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 = get_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 { 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");
}
/// Returns a cloned DirectoryProxy to the dir `dir_string` inside the namespace of
/// `resolved_url`.
pub async fn get_dir_from_namespace(
namespace: &ManagedNamespace,
dir_string: &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_string)
.expect(&format!("didn't find dir {}", dir_string));
let entry = ns.remove(index);
// Clone our directory, and then put the directory and path back on the end of the namespace so
// that the namespace is (somewhat) unmodified.
let dir_proxy = entry.directory.unwrap().into_proxy().unwrap();
let dir_proxy_clone = io_util::clone_directory(&dir_proxy, CLONE_FLAG_SAME_RIGHTS).unwrap();
ns.push(fcrunner::ComponentNamespaceEntry {
path: entry.path,
directory: Some(ClientEnd::new(dir_proxy.into_channel().unwrap().into_zx_channel())),
});
dir_proxy_clone
}
/// 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 realm = model
.look_up_realm(abs_moniker)
.await
.expect(&format!("realm not found {}", abs_moniker));
model.bind(abs_moniker).await.expect("failed to bind instance");
let execution = realm.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(
type_: Option<fsys::StorageType>,
relative_moniker: &RelativeMoniker,
) -> PathBuf {
assert!(relative_moniker.up_path().is_empty());
let mut down_path = relative_moniker.down_path().iter();
let mut dir_path = vec![];
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());
}
match type_ {
Some(fsys::StorageType::Data) => dir_path.push("data".to_string()),
Some(fsys::StorageType::Cache) => dir_path.push("cache".to_string()),
Some(fsys::StorageType::Meta) => dir_path.push("meta".to_string()),
None => {}
}
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")
}
}