blob: d4e8f5773f0641715905ab80125e8b1a8c1eeece [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::directory_broker,
crate::model::{self, error::ModelError},
failure::Fail,
fidl::endpoints::ServerEnd,
fidl_fuchsia_io::{DirectoryProxy, NodeMarker},
fuchsia_async as fasync,
fuchsia_vfs_pseudo_fs::{directory, file::simple::read_only},
futures::{
future::{AbortHandle, Abortable, BoxFuture},
lock::Mutex,
},
std::{collections::HashMap, sync::Arc},
};
/// Hub state on an instance of a component.
struct Instance {
pub abs_moniker: model::AbsoluteMoniker,
pub component_url: String,
pub execution: Option<Execution>,
pub directory: directory::controlled::Controller<'static>,
pub children_directory: directory::controlled::Controller<'static>,
}
/// The execution state for a component that has started running.
struct Execution {
pub resolved_url: String,
pub directory: directory::controlled::Controller<'static>,
}
/// Errors produced by `Hub`.
#[derive(Debug, Fail)]
pub enum HubError {
#[fail(display = "Failed to add directory entry \"{}\" for \"{}\"", abs_moniker, entry_name)]
AddDirectoryEntryError { abs_moniker: model::AbsoluteMoniker, entry_name: String },
}
impl HubError {
pub fn add_directory_entry_error(
abs_moniker: model::AbsoluteMoniker,
entry_name: &str,
) -> HubError {
HubError::AddDirectoryEntryError { abs_moniker, entry_name: entry_name.to_string() }
}
}
pub struct Hub {
instances: Mutex<HashMap<model::AbsoluteMoniker, Instance>>,
/// Called when Hub is dropped to drop pseudodirectory hosting the Hub.
abort_handle: AbortHandle,
}
impl Drop for Hub {
fn drop(&mut self) {
self.abort_handle.abort();
}
}
impl Hub {
/// Create a new Hub given a |component_url| and a controller to the root directory.
pub fn new(
component_url: String,
mut root_directory: directory::simple::Simple<'static>,
) -> Result<Hub, ModelError> {
let mut instances_map = HashMap::new();
let abs_moniker = model::AbsoluteMoniker::root();
let self_directory =
Hub::add_instance_if_necessary(&abs_moniker, component_url, &mut instances_map)?
.expect("Did not create directory.");
root_directory
.add_entry("self", self_directory)
.map_err(|_| HubError::add_directory_entry_error(abs_moniker.clone(), "self"))?;
// Run the hub root directory forever until the component manager is terminated.
let (abort_handle, abort_registration) = AbortHandle::new_pair();
let future = Abortable::new(root_directory, abort_registration);
fasync::spawn(async move {
let _ = await!(future);
});
Ok(Hub { instances: Mutex::new(instances_map), abort_handle })
}
fn add_instance_if_necessary(
abs_moniker: &model::AbsoluteMoniker,
component_url: String,
instance_map: &mut HashMap<model::AbsoluteMoniker, Instance>,
) -> Result<Option<directory::controlled::Controlled<'static>>, HubError> {
if instance_map.contains_key(&abs_moniker) {
return Ok(None);
}
let (controller, mut controlled) =
directory::controlled::controlled(directory::simple::empty());
// Add a 'url' file.
controlled
.add_entry("url", {
let url = component_url.clone();
read_only(move || Ok(url.clone().into_bytes()))
})
.map_err(|_| HubError::add_directory_entry_error(abs_moniker.clone(), "url"))?;
// Add a children directory.
let (children_controller, children_controlled) =
directory::controlled::controlled(directory::simple::empty());
controlled
.add_entry("children", children_controlled)
.map_err(|_| HubError::add_directory_entry_error(abs_moniker.clone(), "children"))?;
instance_map.insert(
abs_moniker.clone(),
Instance {
abs_moniker: abs_moniker.clone(),
component_url,
execution: None,
directory: controller,
children_directory: children_controller,
},
);
Ok(Some(controlled))
}
async fn add_instance_to_parent_if_necessary<'a>(
abs_moniker: &'a model::AbsoluteMoniker,
component_url: String,
mut instances_map: &'a mut HashMap<model::AbsoluteMoniker, Instance>,
) -> Result<(), HubError> {
if let Some(controlled) =
Hub::add_instance_if_necessary(&abs_moniker, component_url, &mut instances_map)?
{
if let (Some(name), Some(parent_moniker)) = (abs_moniker.name(), abs_moniker.parent()) {
await!(instances_map[&parent_moniker]
.children_directory
.add_entry_res(name.clone(), controlled))
.map_err(|_| {
HubError::add_directory_entry_error(abs_moniker.clone(), &name.clone())
})?;
}
}
Ok(())
}
// `route_open_fn` will cause any call on the hub's inserted directory to be
// redirected to the component's 'dir' directory. All directory
// operations other than `Open` will be received by the 'dir'
// directory, because those calls are preceded by an `Open` on a path
// in the hub's insertion point.
fn route_open_fn(
dir: DirectoryProxy,
) -> Box<FnMut(u32, u32, String, ServerEnd<NodeMarker>) + Send> {
Box::new(
move |flags: u32,
mode: u32,
relative_path: String,
server_end: ServerEnd<NodeMarker>| {
// If we want to open the 'dir' directory directly, then call clone.
// Otherwise, pass long the remaining 'relative_path' to the component
// hosting the out directory to resolve.
if !relative_path.is_empty() {
// TODO(fsamuel): Currently DirectoryEntry::open does not return
// a Result so we cannot propagate this error up. We probably
// want to change that.
let _ = dir.open(flags, mode, &relative_path, server_end);
} else {
let _ = dir.clone(flags, server_end);
}
},
)
}
pub async fn on_bind_instance_async<'a>(
&'a self,
realm: Arc<model::Realm>,
realm_state: &'a model::RealmState,
route_fn_factory: model::RoutingFnFactory,
) -> Result<(), ModelError> {
let component_url = realm.component_url.clone();
let abs_moniker = realm.abs_moniker.clone();
let mut instances_map = await!(self.instances.lock());
await!(Self::add_instance_to_parent_if_necessary(
&abs_moniker,
component_url,
&mut instances_map
))?;
let instance = instances_map
.get_mut(&abs_moniker)
.expect(&format!("Unable to find instance {} in map.", abs_moniker));
// If we haven't already created an execution directory, create one now.
if instance.execution.is_none() {
let (controller, mut controlled) =
directory::controlled::controlled(directory::simple::empty());
if let Some(execution) = realm_state.execution.as_ref() {
let exec = Execution {
resolved_url: execution.resolved_url.clone(),
directory: controller,
};
instance.execution = Some(exec);
// Add a 'resolved_url' file.
controlled
.add_entry("resolved_url", {
let resolved_url = execution.resolved_url.clone();
read_only(move || Ok(resolved_url.clone().into_bytes()))
})
.map_err(|_| {
HubError::add_directory_entry_error(abs_moniker.clone(), "resolved_url")
})?;
// Add an 'in' directory.
let decl = realm_state.decl.as_ref().expect("ComponentDecl unavailable.");
let tree =
model::DirTree::build_from_uses(route_fn_factory, &abs_moniker, decl.clone())?;
let mut in_dir = directory::simple::empty();
tree.install(&abs_moniker, &mut in_dir)?;
controlled
.add_entry("in", in_dir)
.map_err(|_| HubError::add_directory_entry_error(abs_moniker.clone(), "in"))?;
// Install the out directory if we can successfully clone it.
// TODO(fsamuel): We should probably preserve the original error messages
// instead of dropping them.
if let Ok(out_dir) = io_util::clone_directory(&execution.outgoing_dir) {
controlled
.add_entry(
"out",
directory_broker::DirectoryBroker::new(Self::route_open_fn(out_dir)),
)
.map_err(|_| {
HubError::add_directory_entry_error(abs_moniker.clone(), "out")
})?;
}
// Install the runtime directory if we can successfully clone it.
// TODO(fsamuel): We should probably preserve the original error messages
// instead of dropping them.
if let Ok(runtime_dir) = io_util::clone_directory(&execution.runtime_dir) {
controlled
.add_entry(
"runtime",
directory_broker::DirectoryBroker::new(Self::route_open_fn(
runtime_dir,
)),
)
.map_err(|_| {
HubError::add_directory_entry_error(abs_moniker.clone(), "runtime")
})?;
}
await!(instance.directory.add_entry_res("exec", controlled)).map_err(|_| {
HubError::add_directory_entry_error(abs_moniker.clone(), "exec")
})?;
}
}
for child_realm in
realm_state.child_realms.as_ref().expect("Unable to access child realms.").values()
{
await!(Self::add_instance_to_parent_if_necessary(
&child_realm.abs_moniker,
child_realm.component_url.clone(),
&mut instances_map
))?;
}
Ok(())
}
}
impl model::Hook for Hub {
fn on_bind_instance<'a>(
&'a self,
realm: Arc<model::Realm>,
realm_state: &'a model::RealmState,
route_fn_factory: model::RoutingFnFactory,
) -> BoxFuture<Result<(), ModelError>> {
Box::pin(self.on_bind_instance_async(realm, realm_state, route_fn_factory))
}
fn on_add_dynamic_child(&self, _realm: Arc<model::Realm>) -> BoxFuture<Result<(), ModelError>> {
// TODO: Update the hub with the new child
Box::pin(async { Ok(()) })
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::model::{
self,
hub::Hub,
testing::mocks,
testing::{
routing_test_helpers::default_component_decl,
test_utils::{dir_contains, list_directory_recursive, read_file},
},
},
cm_rust::{
self, CapabilityPath, ChildDecl, ComponentDecl, UseDecl, UseDirectoryDecl,
UseServiceDecl,
},
fidl::endpoints::{ClientEnd, ServerEnd},
fidl_fuchsia_io::{
DirectoryMarker, DirectoryProxy, NodeMarker, MODE_TYPE_DIRECTORY, OPEN_RIGHT_READABLE,
OPEN_RIGHT_WRITABLE,
},
fidl_fuchsia_sys2 as fsys,
fuchsia_vfs_pseudo_fs::directory::entry::DirectoryEntry,
fuchsia_zircon as zx,
std::{convert::TryFrom, iter, path::PathBuf},
};
/// Hosts an out directory with a 'foo' file.
fn foo_out_dir_fn() -> Box<Fn(ServerEnd<DirectoryMarker>) + Send + Sync> {
Box::new(move |server_end: ServerEnd<DirectoryMarker>| {
let mut out_dir = directory::simple::empty();
// Add a 'foo' file.
out_dir
.add_entry("foo", { read_only(move || Ok(b"bar".to_vec())) })
.map_err(|(s, _)| s)
.expect("Failed to add 'foo' entry");
out_dir.open(
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
MODE_TYPE_DIRECTORY,
&mut iter::empty(),
ServerEnd::new(server_end.into_channel()),
);
let mut test_dir = directory::simple::empty();
test_dir
.add_entry("aaa", { read_only(move || Ok(b"bbb".to_vec())) })
.map_err(|(s, _)| s)
.expect("Failed to add 'aaa' entry");
out_dir
.add_entry("test", test_dir)
.map_err(|(s, _)| s)
.expect("Failed to add 'test' directory.");
fasync::spawn(async move {
let _ = await!(out_dir);
});
})
}
/// Hosts a runtime directory with a 'bleep' file.
fn bleep_runtime_dir_fn() -> Box<Fn(ServerEnd<DirectoryMarker>) + Send + Sync> {
Box::new(move |server_end: ServerEnd<DirectoryMarker>| {
let mut pseudo_dir = directory::simple::empty();
// Add a 'bleep' file.
pseudo_dir
.add_entry("bleep", { read_only(move || Ok(b"blah".to_vec())) })
.map_err(|(s, _)| s)
.expect("Failed to add 'bleep' entry");
pseudo_dir.open(
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
MODE_TYPE_DIRECTORY,
&mut iter::empty(),
ServerEnd::new(server_end.into_channel()),
);
fasync::spawn(async move {
let _ = await!(pseudo_dir);
});
})
}
type DirectoryCallback = Box<Fn(ServerEnd<DirectoryMarker>) + Send + Sync>;
struct ComponentDescriptor {
pub name: String,
pub decl: ComponentDecl,
pub host_fn: Option<DirectoryCallback>,
pub runtime_host_fn: Option<DirectoryCallback>,
}
async fn start_component_manager_with_hub(
root_component_url: String,
components: Vec<ComponentDescriptor>,
) -> (Arc<model::Model>, DirectoryProxy) {
let resolved_root_component_url = format!("{}_resolved", root_component_url);
let mut resolver = model::ResolverRegistry::new();
let mut runner = mocks::MockRunner::new();
let mut mock_resolver = mocks::MockResolver::new();
for component in components.into_iter() {
mock_resolver.add_component(&component.name, component.decl);
if let Some(host_fn) = component.host_fn {
runner.host_fns.insert(resolved_root_component_url.clone(), host_fn);
}
if let Some(runtime_host_fn) = component.runtime_host_fn {
runner
.runtime_host_fns
.insert(resolved_root_component_url.clone(), runtime_host_fn);
}
}
resolver.register("test".to_string(), Box::new(mock_resolver));
let mut hooks: model::Hooks = Vec::new();
let mut root_directory = directory::simple::empty();
let (client_chan, server_chan) = zx::Channel::create().unwrap();
root_directory.open(
OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
0,
&mut iter::empty(),
ServerEnd::<NodeMarker>::new(server_chan.into()),
);
let hub = Arc::new(Hub::new(root_component_url.clone(), root_directory).unwrap());
hooks.push(hub);
let model = Arc::new(model::Model::new(model::ModelParams {
ambient: Box::new(mocks::MockAmbientEnvironment::new()),
root_component_url,
root_resolver_registry: resolver,
root_default_runner: Box::new(runner),
hooks,
}));
let res = await!(model.look_up_and_bind_instance(model::AbsoluteMoniker::root()));
let expected_res: Result<(), model::ModelError> = Ok(());
assert_eq!(format!("{:?}", res), format!("{:?}", expected_res));
let hub_proxy = ClientEnd::<DirectoryMarker>::new(client_chan)
.into_proxy()
.expect("failed to create directory proxy");
(model, hub_proxy)
}
#[fuchsia_async::run_singlethreaded(test)]
async fn hub_basic() {
let root_component_url = "test:///root".to_string();
let (_model, hub_proxy) = await!(start_component_manager_with_hub(
root_component_url.clone(),
vec![
ComponentDescriptor {
name: "root".to_string(),
decl: ComponentDecl {
children: vec![ChildDecl {
name: "a".to_string(),
url: "test:///a".to_string(),
startup: fsys::StartupMode::Lazy,
}],
..default_component_decl()
},
host_fn: None,
runtime_host_fn: None
},
ComponentDescriptor {
name: "a".to_string(),
decl: ComponentDecl { children: vec![], ..default_component_decl() },
host_fn: None,
runtime_host_fn: None
}
]
));
assert_eq!(root_component_url, await!(read_file(&hub_proxy, "self/url")));
assert_eq!(
format!("{}_resolved", root_component_url),
await!(read_file(&hub_proxy, "self/exec/resolved_url"))
);
assert_eq!("test:///a", await!(read_file(&hub_proxy, "self/children/a/url")));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn hub_out_directory() {
let root_component_url = "test:///root".to_string();
let (_model, hub_proxy) = await!(start_component_manager_with_hub(
root_component_url.clone(),
vec![ComponentDescriptor {
name: "root".to_string(),
decl: ComponentDecl {
children: vec![ChildDecl {
name: "a".to_string(),
url: "test:///a".to_string(),
startup: fsys::StartupMode::Lazy,
}],
..default_component_decl()
},
host_fn: Some(foo_out_dir_fn()),
runtime_host_fn: None
}]
));
assert!(await!(dir_contains(&hub_proxy, "self/exec", "out")));
assert!(await!(dir_contains(&hub_proxy, "self/exec/out", "foo")));
assert!(await!(dir_contains(&hub_proxy, "self/exec/out/test", "aaa")));
assert_eq!("bar", await!(read_file(&hub_proxy, "self/exec/out/foo")));
assert_eq!("bbb", await!(read_file(&hub_proxy, "self/exec/out/test/aaa")));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn hub_runtime_directory() {
let root_component_url = "test:///root".to_string();
let (_model, hub_proxy) = await!(start_component_manager_with_hub(
root_component_url.clone(),
vec![ComponentDescriptor {
name: "root".to_string(),
decl: ComponentDecl {
children: vec![ChildDecl {
name: "a".to_string(),
url: "test:///a".to_string(),
startup: fsys::StartupMode::Lazy,
}],
..default_component_decl()
},
host_fn: None,
runtime_host_fn: Some(bleep_runtime_dir_fn())
}]
));
assert_eq!("blah", await!(read_file(&hub_proxy, "self/exec/runtime/bleep")));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn hub_in_directory() {
let root_component_url = "test:///root".to_string();
let (_model, hub_proxy) = await!(start_component_manager_with_hub(
root_component_url.clone(),
vec![ComponentDescriptor {
name: "root".to_string(),
decl: ComponentDecl {
children: vec![ChildDecl {
name: "a".to_string(),
url: "test:///a".to_string(),
startup: fsys::StartupMode::Lazy,
}],
uses: vec![
UseDecl::Directory(UseDirectoryDecl {
source_path: CapabilityPath::try_from("/data/baz").unwrap(),
target_path: CapabilityPath::try_from("/data/hippo").unwrap(),
}),
UseDecl::Service(UseServiceDecl {
source_path: CapabilityPath::try_from("/svc/baz").unwrap(),
target_path: CapabilityPath::try_from("/svc/hippo").unwrap(),
}),
UseDecl::Directory(UseDirectoryDecl {
source_path: CapabilityPath::try_from("/data/foo").unwrap(),
target_path: CapabilityPath::try_from("/data/bar").unwrap(),
}),
],
..default_component_decl()
},
host_fn: None,
runtime_host_fn: None,
}]
));
let in_dir = io_util::open_directory(&hub_proxy, &PathBuf::from("self/exec/in"))
.expect("Failed to open directory");
assert_eq!(
vec!["data/bar", "data/hippo", "svc/hippo"],
await!(list_directory_recursive(&in_dir))
);
}
}