blob: 85220d96e69b533288b41739ef75f4679f2c6121 [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.cti
use {
crate::{
capability::{CapabilityProvider, FrameworkCapability, InternalCapabilityProvider},
model::{
component::{ComponentInstance, WeakComponentInstance},
error::OpenExposedDirError,
model::Model,
},
},
::routing::capability_source::InternalCapability,
anyhow::Error,
async_trait::async_trait,
cm_config::RuntimeConfig,
cm_rust::FidlIntoNative,
cm_types::{Name, OPEN_FLAGS_MAX_POSSIBLE_RIGHTS},
fidl::endpoints::ServerEnd,
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_io as fio, fuchsia_async as fasync, fuchsia_zircon as zx,
futures::prelude::*,
lazy_static::lazy_static,
moniker::{ChildName, ChildNameBase, Moniker},
std::{
cmp,
sync::{Arc, Weak},
},
tracing::{debug, error, warn},
vfs::{directory::entry::OpenRequest, path::Path, ToObjectRequest},
};
lazy_static! {
static ref REALM_SERVICE: Name = "fuchsia.component.Realm".parse().unwrap();
}
struct RealmCapabilityProvider {
scope_moniker: Moniker,
host: Arc<RealmCapabilityHost>,
}
impl RealmCapabilityProvider {
pub fn new(scope_moniker: Moniker, host: Arc<RealmCapabilityHost>) -> Self {
Self { scope_moniker, host }
}
}
#[async_trait]
impl InternalCapabilityProvider for RealmCapabilityProvider {
async fn open_protocol(self: Box<Self>, server_end: zx::Channel) {
let server_end = ServerEnd::<fcomponent::RealmMarker>::new(server_end);
// We only need to look up the component matching this scope.
// These operations should all work, even if the component is not running.
if let Some(model) = self.host.model.upgrade() {
if let Ok(component) = model.root().find_and_maybe_resolve(&self.scope_moniker).await {
let weak = WeakComponentInstance::new(&component);
drop(component);
let serve_result = self.host.serve(weak, server_end.into_stream().unwrap()).await;
if let Err(error) = serve_result {
// TODO: Set an epitaph to indicate this was an unexpected error.
warn!(%error, "serve failed");
}
}
}
}
}
pub struct RealmFrameworkCapability {
host: Arc<RealmCapabilityHost>,
}
pub struct RealmCapabilityHost {
model: Weak<Model>,
config: Arc<RuntimeConfig>,
}
impl RealmFrameworkCapability {
pub fn new(model: Weak<Model>, config: Arc<RuntimeConfig>) -> Self {
Self { host: Arc::new(RealmCapabilityHost::new(model, config)) }
}
}
impl FrameworkCapability for RealmFrameworkCapability {
fn matches(&self, capability: &InternalCapability) -> bool {
capability.matches_protocol(&REALM_SERVICE)
}
fn new_provider(
&self,
scope: WeakComponentInstance,
_target: WeakComponentInstance,
) -> Box<dyn CapabilityProvider> {
Box::new(RealmCapabilityProvider::new(scope.moniker.clone(), self.host.clone()))
}
}
// `RealmCapabilityHost` is a `Hook` that serves the `Realm` FIDL protocol.
impl RealmCapabilityHost {
fn new(model: Weak<Model>, config: Arc<RuntimeConfig>) -> Self {
Self { model, config }
}
#[cfg(test)]
pub fn new_for_test(model: Weak<Model>, config: Arc<RuntimeConfig>) -> Self {
Self::new(model, config)
}
pub async fn serve(
&self,
component: WeakComponentInstance,
mut stream: fcomponent::RealmRequestStream,
) -> Result<(), fidl::Error> {
while let Some(request) = stream.try_next().await? {
let method_name = request.method_name();
let result = self.handle_request(request, &component).await;
match result {
// If the error was PEER_CLOSED then we don't need to log it as a client can
// disconnect while we are processing its request.
Err(error) if !error.is_closed() => {
warn!(%method_name, %error, "Couldn't send Realm response");
}
_ => {}
}
}
Ok(())
}
async fn handle_request(
&self,
request: fcomponent::RealmRequest,
component: &WeakComponentInstance,
) -> Result<(), fidl::Error> {
match request {
fcomponent::RealmRequest::CreateChild { responder, collection, decl, args } => {
let res =
async { Self::create_child(component, collection, decl, args).await }.await;
responder.send(res)?;
}
fcomponent::RealmRequest::DestroyChild { responder, child } => {
let res = Self::destroy_child(component, child).await;
responder.send(res)?;
}
fcomponent::RealmRequest::ListChildren { responder, collection, iter } => {
let res = Self::list_children(
component,
self.config.list_children_batch_size,
collection,
iter,
)
.await;
responder.send(res)?;
}
fcomponent::RealmRequest::OpenExposedDir { responder, child, exposed_dir } => {
let res = Self::open_exposed_dir(component, child, exposed_dir).await;
responder.send(res)?;
}
}
Ok(())
}
pub async fn create_child(
component: &WeakComponentInstance,
collection: fdecl::CollectionRef,
child_decl: fdecl::Child,
child_args: fcomponent::CreateChildArgs,
) -> Result<(), fcomponent::Error> {
let component = component.upgrade().map_err(|_| fcomponent::Error::InstanceDied)?;
cm_fidl_validator::validate_dynamic_child(&child_decl).map_err(|error| {
warn!(%error, "failed to create dynamic child. child decl is invalid");
fcomponent::Error::InvalidArguments
})?;
let child_decl = child_decl.fidl_into_native();
component.add_dynamic_child(collection.name.clone(), &child_decl, child_args).await.map_err(
|err| {
warn!(
"Failed to create child \"{}\" in collection \"{}\" of component \"{}\": {}",
child_decl.name, collection.name, component.moniker, err
);
err.into()
},
)
}
async fn open_exposed_dir(
component: &WeakComponentInstance,
child: fdecl::ChildRef,
exposed_dir: ServerEnd<fio::DirectoryMarker>,
) -> Result<(), fcomponent::Error> {
match Self::get_child(component, child.clone()).await? {
Some(child) => {
// Resolve child in order to instantiate exposed_dir.
child.resolve().await.map_err(|e| {
warn!(
"resolve failed for child {:?} of component {}: {}",
child, component.moniker, e
);
return fcomponent::Error::InstanceCannotResolve;
})?;
// TODO(https://fxbug.dev/42161419): open_exposed does not have a rights input
// parameter, so this makes use of the POSIX_[WRITABLE|EXECUTABLE] flags to open a
// connection with those rights if available from the parent directory connection
// but without failing if not available.
let flags = OPEN_FLAGS_MAX_POSSIBLE_RIGHTS | fio::OpenFlags::DIRECTORY;
let mut object_request = flags.to_object_request(exposed_dir);
child
.open_exposed(OpenRequest::new(
child.execution_scope.clone(),
flags,
Path::dot(),
&mut object_request,
))
.await
.map_err(|error| match error {
OpenExposedDirError::InstanceDestroyed
| OpenExposedDirError::InstanceNotResolved => {
fcomponent::Error::InstanceDied
}
OpenExposedDirError::Open(_) => fcomponent::Error::Internal,
})?;
}
None => {
debug!(?child, "open_exposed_dir() failed: instance not found");
return Err(fcomponent::Error::InstanceNotFound);
}
}
Ok(())
}
pub async fn destroy_child(
component: &WeakComponentInstance,
child: fdecl::ChildRef,
) -> Result<(), fcomponent::Error> {
let component = component.upgrade().map_err(|_| fcomponent::Error::InstanceDied)?;
child.collection.as_ref().ok_or(fcomponent::Error::InvalidArguments)?;
let child_moniker = ChildName::try_new(&child.name, child.collection.as_ref())
.map_err(|_| fcomponent::Error::InvalidArguments)?;
component.remove_dynamic_child(&child_moniker).await.map_err(|error| {
debug!(%error, ?child, "remove_dynamic_child() failed");
error
})?;
Ok(())
}
async fn get_child(
parent: &WeakComponentInstance,
child: fdecl::ChildRef,
) -> Result<Option<Arc<ComponentInstance>>, fcomponent::Error> {
let parent = parent.upgrade().map_err(|_| fcomponent::Error::InstanceDied)?;
let state = parent.lock_resolved_state().await.map_err(|error| {
debug!(%error, moniker=%parent.moniker, "failed to resolve instance");
fcomponent::Error::InstanceCannotResolve
})?;
let child_moniker = ChildName::try_new(&child.name, child.collection.as_ref())
.map_err(|_| fcomponent::Error::InvalidArguments)?;
Ok(state.get_child(&child_moniker).map(|r| r.clone()))
}
async fn list_children(
component: &WeakComponentInstance,
batch_size: usize,
collection: fdecl::CollectionRef,
iter: ServerEnd<fcomponent::ChildIteratorMarker>,
) -> Result<(), fcomponent::Error> {
let component = component.upgrade().map_err(|_| fcomponent::Error::InstanceDied)?;
let state = component.lock_resolved_state().await.map_err(|error| {
error!(%error, "failed to resolve InstanceState");
fcomponent::Error::Internal
})?;
let decl = state.decl();
decl.find_collection(&collection.name).ok_or(fcomponent::Error::CollectionNotFound)?;
let mut children: Vec<_> = state
.children()
.filter_map(|(m, _)| match m.collection() {
Some(c) => {
if c.as_str() == &collection.name {
Some(fdecl::ChildRef {
name: m.name().to_string(),
collection: m.collection().map(|s| s.to_string()),
})
} else {
None
}
}
_ => None,
})
.collect();
children.sort_unstable_by(|a, b| {
let a = &a.name;
let b = &b.name;
if a == b {
cmp::Ordering::Equal
} else if a < b {
cmp::Ordering::Less
} else {
cmp::Ordering::Greater
}
});
let stream = iter.into_stream().map_err(|_| fcomponent::Error::AccessDenied)?;
fasync::Task::spawn(async move {
if let Err(error) = Self::serve_child_iterator(children, stream, batch_size).await {
// TODO: Set an epitaph to indicate this was an unexpected error.
warn!(%error, "serve_child_iterator failed");
}
})
.detach();
Ok(())
}
async fn serve_child_iterator(
children: Vec<fdecl::ChildRef>,
mut stream: fcomponent::ChildIteratorRequestStream,
batch_size: usize,
) -> Result<(), Error> {
let mut iter = children.chunks(batch_size);
while let Some(request) = stream.try_next().await? {
match request {
fcomponent::ChildIteratorRequest::Next { responder } => {
responder.send(iter.next().unwrap_or(&[]))?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::{
builtin_environment::BuiltinEnvironment,
model::{
component::StartReason,
events::{source::EventSource, stream::EventStream},
hooks::EventType,
testing::{mocks::*, out_dir::OutDir, test_helpers::*, test_hook::*},
},
},
assert_matches::assert_matches,
cm_rust::{ComponentDecl, ExposeSource},
cm_rust_testing::*,
fidl::endpoints,
fidl_fidl_examples_routing_echo as echo, fidl_fuchsia_component as fcomponent,
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio, fuchsia_async as fasync,
fuchsia_component::client,
futures::lock::Mutex,
moniker::MonikerBase,
routing_test_helpers::component_decl_with_exposed_binder,
std::collections::HashSet,
};
struct RealmCapabilityTest {
builtin_environment: Option<Arc<Mutex<BuiltinEnvironment>>>,
mock_runner: Arc<MockRunner>,
component: Option<Arc<ComponentInstance>>,
realm_proxy: fcomponent::RealmProxy,
hook: Arc<TestHook>,
}
impl RealmCapabilityTest {
async fn new(
components: Vec<(&'static str, ComponentDecl)>,
component_moniker: Moniker,
) -> Self {
// Init model.
let config = RuntimeConfig { list_children_batch_size: 2, ..Default::default() };
let TestModelResult { model, builtin_environment, mock_runner, .. } =
TestEnvironmentBuilder::new()
.set_runtime_config(config)
.set_components(components)
.build()
.await;
let hook = Arc::new(TestHook::new());
let hooks = hook.hooks();
// Install TestHook at the front so that when we receive an event the hook has already
// run so the result is reflected in its printout
model.root().hooks.install_front(hooks).await;
// Look up and start component.
let component = model
.root()
.start_instance(&component_moniker, &StartReason::Eager)
.await
.expect("failed to start component");
// Host framework service.
let (realm_proxy, stream) =
endpoints::create_proxy_and_stream::<fcomponent::RealmMarker>().unwrap();
{
let component = WeakComponentInstance::from(&component);
let realm_capability_host = RealmCapabilityHost::new(
Arc::downgrade(&model),
model.context().runtime_config().clone(),
);
fasync::Task::spawn(async move {
realm_capability_host
.serve(component, stream)
.await
.expect("failed serving realm service");
})
.detach();
}
Self {
builtin_environment: Some(builtin_environment),
mock_runner,
component: Some(component),
realm_proxy,
hook,
}
}
fn component(&self) -> &Arc<ComponentInstance> {
self.component.as_ref().unwrap()
}
fn drop_component(&mut self) {
self.component = None;
self.builtin_environment = None;
}
async fn new_event_stream(&self, events: Vec<Name>) -> (EventSource, EventStream) {
new_event_stream(
self.builtin_environment.as_ref().expect("builtin_environment is none").clone(),
events,
)
.await
}
}
fn child_decl(name: &str) -> fdecl::Child {
fdecl::Child {
name: Some(name.to_owned()),
url: Some(format!("test:///{}", name)),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
on_terminate: None,
..Default::default()
}
}
#[fuchsia::test]
async fn create_dynamic_child() {
// Set up model and realm service.
let test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().child_default("system").build()),
(
"system",
ComponentDeclBuilder::new()
.collection(CollectionBuilder::new().name("coll").allow_long_names())
.build(),
),
// Eagerly launched so it needs a definition
("b", ComponentDeclBuilder::new().build()),
],
vec!["system"].try_into().unwrap(),
)
.await;
let (_event_source, mut event_stream) = test
.new_event_stream(vec![EventType::Discovered.into(), EventType::Started.into()])
.await;
// Test that a dynamic child with a long name can also be created.
let long_name = &"c".repeat(cm_types::MAX_LONG_NAME_LENGTH);
// Create children "a", "b", and "<long_name>" in collection. Expect a Discovered event for
// each.
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
{
// Create a child
test.realm_proxy
.create_child(
&collection_ref,
&child_decl("a"),
fcomponent::CreateChildArgs::default(),
)
.await
.unwrap()
.unwrap();
// Ensure that an event exists for the new child
event_stream
.wait_until(EventType::Discovered, "system/coll:a".try_into().unwrap())
.await
.unwrap();
}
{
// Create a child (eager)
let mut child_decl = child_decl("b");
child_decl.startup = Some(fdecl::StartupMode::Eager);
test.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await
.unwrap()
.unwrap();
// Ensure that an event exists for the new child
event_stream
.wait_until(EventType::Discovered, "system/coll:b".try_into().unwrap())
.await
.unwrap();
event_stream
.wait_until(EventType::Started, "system/coll:b".try_into().unwrap())
.await
.unwrap();
}
{
// Create a child (long name)
test.realm_proxy
.create_child(
&collection_ref,
&child_decl(long_name),
fcomponent::CreateChildArgs::default(),
)
.await
.unwrap()
.unwrap();
// Ensure that an event exists for the new child
event_stream
.wait_until(
EventType::Discovered,
format!("system/coll:{long_name}").as_str().try_into().unwrap(),
)
.await
.unwrap();
}
// Verify that the component topology matches expectations.
let actual_children = get_live_children(test.component()).await;
let mut expected_children: HashSet<ChildName> = HashSet::new();
expected_children.insert("coll:a".try_into().unwrap());
expected_children.insert("coll:b".try_into().unwrap());
expected_children.insert(format!("coll:{}", long_name).as_str().try_into().unwrap());
assert_eq!(actual_children, expected_children);
assert_eq!(format!("(system(coll:a,coll:b,coll:{}))", long_name), test.hook.print());
}
#[fuchsia::test]
async fn create_dynamic_child_errors() {
let mut test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().child_default("system").build()),
(
"system",
ComponentDeclBuilder::new()
.collection_default("coll")
.collection(
CollectionBuilder::new()
.name("pcoll")
.durability(fdecl::Durability::Transient)
.allow_long_names(),
)
.collection(
CollectionBuilder::new()
.name("dynoff")
.allowed_offers(cm_types::AllowedOffers::StaticAndDynamic),
)
.build(),
),
],
vec!["system"].try_into().unwrap(),
)
.await;
// Invalid arguments.
{
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let child_decl = fdecl::Child {
name: Some("a".to_string()),
url: None,
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
{
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let child_decl = fdecl::Child {
name: Some("a".to_string()),
url: Some("test:///a".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: Some("env".to_string()),
..Default::default()
};
let err = test
.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Long dynamic child name violations.
{
// Name exceeds MAX_NAME_LENGTH when `allow_long_names` is not set.
// The FIDL call succeeds but the server responds with an error.
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let child_decl = fdecl::Child {
name: Some("a".repeat(cm_types::MAX_NAME_LENGTH + 1).to_string()),
url: None,
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
// Name length exceeds the MAX_LONG_NAME_LENGTH when `allow_long_names` is set.
// In this case the FIDL call fails to encode because the name field
// is defined in the FIDL library as `string:MAX_LONG_NAME_LENGTH`.
let collection_ref = fdecl::CollectionRef { name: "pcoll".to_string() };
let child_decl = fdecl::Child {
name: Some("a".repeat(cm_types::MAX_LONG_NAME_LENGTH + 1).to_string()),
url: None,
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await
.expect_err("unexpected success");
// When exceeding the long max name length, the FIDL call itself
// fails because the name field is defined as `string:1024`.
assert_matches!(err, fidl::Error::StringTooLong { .. });
}
// Instance already exists.
{
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let res = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl("a"),
fcomponent::CreateChildArgs::default(),
)
.await;
res.expect("fidl call failed").expect("failed to create child a");
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl("a"),
fcomponent::CreateChildArgs::default(),
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceAlreadyExists);
}
// Collection not found.
{
let collection_ref = fdecl::CollectionRef { name: "nonexistent".to_string() };
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl("a"),
fcomponent::CreateChildArgs::default(),
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::CollectionNotFound);
}
fn sample_offer_from(source: fdecl::Ref) -> fdecl::Offer {
fdecl::Offer::Protocol(fdecl::OfferProtocol {
source: Some(source),
source_name: Some("foo".to_string()),
target_name: Some("foo".to_string()),
dependency_type: Some(fdecl::DependencyType::Strong),
..Default::default()
})
}
// Disallowed dynamic offers specified.
{
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl,
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![sample_offer_from(fdecl::Ref::Parent(
fdecl::ParentRef {},
))]),
..Default::default()
},
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Malformed dynamic offers specified.
{
let collection_ref = fdecl::CollectionRef { name: "dynoff".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl,
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Protocol(fdecl::OfferProtocol {
source: Some(fdecl::Ref::Parent(fdecl::ParentRef {})),
source_name: Some("foo".to_string()),
target_name: Some("foo".to_string()),
// Note: has no `dependency_type`.
..Default::default()
})]),
..Default::default()
},
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Dynamic offer source is a static component that doesn't exist.
{
let collection_ref = fdecl::CollectionRef { name: "dynoff".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl,
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![sample_offer_from(fdecl::Ref::Child(
fdecl::ChildRef {
name: "does_not_exist".to_string(),
collection: None,
},
))]),
..Default::default()
},
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Source is a collection that doesn't exist (and using a Service).
{
let collection_ref = fdecl::CollectionRef { name: "dynoff".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl,
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![fdecl::Offer::Service(fdecl::OfferService {
source: Some(fdecl::Ref::Collection(fdecl::CollectionRef {
name: "does_not_exist".to_string(),
})),
source_name: Some("foo".to_string()),
target_name: Some("foo".to_string()),
..Default::default()
})]),
..Default::default()
},
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Source is a component in the same collection that doesn't exist.
{
let collection_ref = fdecl::CollectionRef { name: "dynoff".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl,
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![sample_offer_from(fdecl::Ref::Child(
fdecl::ChildRef {
name: "does_not_exist".to_string(),
collection: Some("dynoff".to_string()),
},
))]),
..Default::default()
},
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Source is the component itself... which doesn't exist... yet.
{
let collection_ref = fdecl::CollectionRef { name: "dynoff".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl,
fcomponent::CreateChildArgs {
dynamic_offers: Some(vec![sample_offer_from(fdecl::Ref::Child(
fdecl::ChildRef {
name: "b".to_string(),
collection: Some("dynoff".to_string()),
},
))]),
..Default::default()
},
)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Instance died.
{
test.drop_component();
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let child_decl = fdecl::Child {
name: Some("b".to_string()),
url: Some("test:///b".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let err = test
.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceDied);
}
}
#[fuchsia::test]
async fn destroy_dynamic_child() {
// Set up model and realm service.
let test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().child_default("system").build()),
("system", ComponentDeclBuilder::new().collection_default("coll").build()),
("a", component_decl_with_exposed_binder()),
("b", component_decl_with_exposed_binder()),
],
vec!["system"].try_into().unwrap(),
)
.await;
let (_event_source, mut event_stream) = test
.new_event_stream(vec![EventType::Stopped.into(), EventType::Destroyed.into()])
.await;
// Create children "a" and "b" in collection, and start them.
for name in &["a", "b"] {
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let res = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl(name),
fcomponent::CreateChildArgs::default(),
)
.await;
res.expect("fidl call failed")
.unwrap_or_else(|_| panic!("failed to create child {}", name));
let child_ref =
fdecl::ChildRef { name: name.to_string(), collection: Some("coll".to_string()) };
let (exposed_dir, server_end) =
endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let () = test
.realm_proxy
.open_exposed_dir(&child_ref, server_end)
.await
.expect("OpenExposedDir FIDL")
.expect("OpenExposedDir Error");
let _: fcomponent::BinderProxy =
client::connect_to_protocol_at_dir_root::<fcomponent::BinderMarker>(&exposed_dir)
.expect("Connection to fuchsia.component.Binder");
}
let child = get_live_child(test.component(), "coll:a").await;
let instance_id = get_incarnation_id(test.component(), "coll:a").await;
assert_eq!("(system(coll:a,coll:b))", test.hook.print());
assert_eq!(child.component_url, "test:///a".to_string());
assert_eq!(instance_id, 1);
// Destroy "a". "a" is no longer live from the client's perspective, although it's still
// being destroyed.
let child_ref =
fdecl::ChildRef { name: "a".to_string(), collection: Some("coll".to_string()) };
let (f, destroy_handle) = test.realm_proxy.destroy_child(&child_ref).remote_handle();
fasync::Task::spawn(f).detach();
// The component should be stopped (shut down) before it is destroyed.
event_stream
.wait_until(EventType::Stopped, vec!["system", "coll:a"].try_into().unwrap())
.await
.unwrap();
event_stream
.wait_until(EventType::Destroyed, vec!["system", "coll:a"].try_into().unwrap())
.await
.unwrap();
// "a" is fully deleted now.
assert!(!has_child(test.component(), "coll:a").await);
{
let actual_children = get_live_children(test.component()).await;
let mut expected_children: HashSet<ChildName> = HashSet::new();
expected_children.insert("coll:b".try_into().unwrap());
let child_b = get_live_child(test.component(), "coll:b").await;
assert!(!execution_is_shut_down(&child_b).await);
assert_eq!(actual_children, expected_children);
assert_eq!("(system(coll:b))", test.hook.print());
}
let res = destroy_handle.await;
res.expect("fidl call failed").expect("failed to destroy child a");
// Recreate "a" and verify "a" is back (but it's a different "a"). The old "a" is gone
// from the client's point of view, but it hasn't been cleaned up yet.
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let child_decl = fdecl::Child {
name: Some("a".to_string()),
url: Some("test:///a_alt".to_string()),
startup: Some(fdecl::StartupMode::Lazy),
environment: None,
..Default::default()
};
let res = test
.realm_proxy
.create_child(&collection_ref, &child_decl, fcomponent::CreateChildArgs::default())
.await;
res.expect("fidl call failed").expect("failed to recreate child a");
assert_eq!("(system(coll:a,coll:b))", test.hook.print());
let child = get_live_child(test.component(), "coll:a").await;
let instance_id = get_incarnation_id(test.component(), "coll:a").await;
assert_eq!(child.component_url, "test:///a_alt".to_string());
assert_eq!(instance_id, 3);
}
#[fuchsia::test]
async fn destroy_dynamic_child_errors() {
let mut test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().child_default("system").build()),
("system", ComponentDeclBuilder::new().collection_default("coll").build()),
],
vec!["system"].try_into().unwrap(),
)
.await;
// Create child "a" in collection.
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let res = test
.realm_proxy
.create_child(&collection_ref, &child_decl("a"), fcomponent::CreateChildArgs::default())
.await;
res.expect("fidl call failed").expect("failed to create child a");
// Invalid arguments.
{
let child_ref = fdecl::ChildRef { name: "a".to_string(), collection: None };
let err = test
.realm_proxy
.destroy_child(&child_ref)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InvalidArguments);
}
// Instance not found.
{
let child_ref =
fdecl::ChildRef { name: "b".to_string(), collection: Some("coll".to_string()) };
let err = test
.realm_proxy
.destroy_child(&child_ref)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceNotFound);
}
// Instance died.
{
test.drop_component();
let child_ref =
fdecl::ChildRef { name: "a".to_string(), collection: Some("coll".to_string()) };
let err = test
.realm_proxy
.destroy_child(&child_ref)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceDied);
}
}
#[fuchsia::test]
async fn dynamic_single_run_child() {
// Set up model and realm service.
let test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().child_default("system").build()),
(
"system",
ComponentDeclBuilder::new()
.collection(
CollectionBuilder::new()
.name("coll")
.durability(fdecl::Durability::SingleRun),
)
.build(),
),
("a", component_decl_with_test_runner()),
],
vec!["system"].try_into().unwrap(),
)
.await;
let (_event_source, mut event_stream) = test
.new_event_stream(vec![EventType::Started.into(), EventType::Destroyed.into()])
.await;
// Create child "a" in collection. Expect a Started event.
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
test.realm_proxy
.create_child(&collection_ref, &child_decl("a"), fcomponent::CreateChildArgs::default())
.await
.unwrap()
.unwrap();
event_stream
.wait_until(EventType::Started, vec!["system", "coll:a"].try_into().unwrap())
.await
.unwrap();
// Started action completes.
let child = {
let state = test.component().lock_resolved_state().await.unwrap();
let child = state.children().next().unwrap();
assert_eq!("a", child.0.name());
child.1.clone()
};
// The stop should trigger a delete/purge.
child.stop_instance_internal(false).await.unwrap();
event_stream
.wait_until(EventType::Destroyed, vec!["system", "coll:a"].try_into().unwrap())
.await
.unwrap();
// Verify that the component topology matches expectations.
let actual_children = get_live_children(test.component()).await;
let expected_children: HashSet<ChildName> = HashSet::new();
assert_eq!(actual_children, expected_children);
}
#[fuchsia::test]
async fn list_children_errors() {
// Create a root component with a collection.
let mut test = RealmCapabilityTest::new(
vec![("root", ComponentDeclBuilder::new().collection_default("coll").build())],
Moniker::root(),
)
.await;
// Collection not found.
{
let collection_ref = fdecl::CollectionRef { name: "nonexistent".to_string() };
let (_, server_end) = endpoints::create_proxy().unwrap();
let err = test
.realm_proxy
.list_children(&collection_ref, server_end)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::CollectionNotFound);
}
// Instance died.
{
test.drop_component();
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let (_, server_end) = endpoints::create_proxy().unwrap();
let err = test
.realm_proxy
.list_children(&collection_ref, server_end)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceDied);
}
}
#[fuchsia::test]
async fn open_exposed_dir() {
let test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().child_default("system").build()),
(
"system",
ComponentDeclBuilder::new()
.protocol_default("foo")
.expose(
ExposeBuilder::protocol()
.name("foo")
.target_name("hippo")
.source(ExposeSource::Self_),
)
.build(),
),
],
Moniker::root(),
)
.await;
let (_event_source, mut event_stream) = test
.new_event_stream(vec![EventType::Resolved.into(), EventType::Started.into()])
.await;
let mut out_dir = OutDir::new();
out_dir.add_echo_protocol("/svc/foo".parse().unwrap());
test.mock_runner.add_host_fn("test:///system_resolved", out_dir.host_fn());
// Open exposed directory of child.
let child_ref = fdecl::ChildRef { name: "system".to_string(), collection: None };
let (dir_proxy, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let res = test.realm_proxy.open_exposed_dir(&child_ref, server_end).await;
res.expect("fidl call failed").expect("open_exposed_dir() failed");
// Assert that child was resolved.
let event =
event_stream.wait_until(EventType::Resolved, vec!["system"].try_into().unwrap()).await;
assert!(event.is_some());
// Assert that event stream doesn't have any outstanding messages.
// This ensures that EventType::Started for "system" has not been
// registered.
let event = event_stream
.wait_until(EventType::Started, vec!["system"].try_into().unwrap())
.now_or_never();
assert!(event.is_none());
// Now that it was asserted that "system:0" has yet to start,
// assert that it starts after making connection below.
let echo_proxy =
client::connect_to_named_protocol_at_dir_root::<echo::EchoMarker>(&dir_proxy, "hippo")
.expect("failed to open hippo service");
let event =
event_stream.wait_until(EventType::Started, vec!["system"].try_into().unwrap()).await;
assert!(event.is_some());
let res = echo_proxy.echo_string(Some("hippos")).await;
assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string()));
// Verify topology matches expectations.
let expected_urls = &["test:///root_resolved", "test:///system_resolved"];
test.mock_runner.wait_for_urls(expected_urls).await;
assert_eq!("(system)", test.hook.print());
}
#[fuchsia::test]
async fn open_exposed_dir_dynamic_child() {
let test = RealmCapabilityTest::new(
vec![
("root", ComponentDeclBuilder::new().collection_default("coll").build()),
(
"system",
ComponentDeclBuilder::new()
.protocol_default("foo")
.expose(
ExposeBuilder::protocol()
.name("foo")
.target_name("hippo")
.source(ExposeSource::Self_),
)
.build(),
),
],
Moniker::root(),
)
.await;
let (_event_source, mut event_stream) = test
.new_event_stream(vec![EventType::Resolved.into(), EventType::Started.into()])
.await;
let mut out_dir = OutDir::new();
out_dir.add_echo_protocol("/svc/foo".parse().unwrap());
test.mock_runner.add_host_fn("test:///system_resolved", out_dir.host_fn());
// Add "system" to collection.
let collection_ref = fdecl::CollectionRef { name: "coll".to_string() };
let res = test
.realm_proxy
.create_child(
&collection_ref,
&child_decl("system"),
fcomponent::CreateChildArgs::default(),
)
.await;
res.expect("fidl call failed").expect("failed to create child system");
// Open exposed directory of child.
let child_ref =
fdecl::ChildRef { name: "system".to_string(), collection: Some("coll".to_owned()) };
let (dir_proxy, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let res = test.realm_proxy.open_exposed_dir(&child_ref, server_end).await;
res.expect("fidl call failed").expect("open_exposed_dir() failed");
// Assert that child was resolved.
let event = event_stream
.wait_until(EventType::Resolved, vec!["coll:system"].try_into().unwrap())
.await;
assert!(event.is_some());
// Assert that event stream doesn't have any outstanding messages.
// This ensures that EventType::Started for "system" has not been
// registered.
let event = event_stream
.wait_until(EventType::Started, vec!["coll:system"].try_into().unwrap())
.now_or_never();
assert!(event.is_none());
// Now that it was asserted that "system" has yet to start,
// assert that it starts after making connection below.
let echo_proxy =
client::connect_to_named_protocol_at_dir_root::<echo::EchoMarker>(&dir_proxy, "hippo")
.expect("failed to open hippo service");
let event = event_stream
.wait_until(EventType::Started, vec!["coll:system"].try_into().unwrap())
.await;
assert!(event.is_some());
let res = echo_proxy.echo_string(Some("hippos")).await;
assert_eq!(res.expect("failed to use echo service"), Some("hippos".to_string()));
// Verify topology matches expectations.
let expected_urls = &["test:///root_resolved", "test:///system_resolved"];
test.mock_runner.wait_for_urls(expected_urls).await;
assert_eq!("(coll:system)", test.hook.print());
}
#[fuchsia::test]
async fn open_exposed_dir_errors() {
let mut test = RealmCapabilityTest::new(
vec![
(
"root",
ComponentDeclBuilder::new()
.child_default("system")
.child_default("unresolvable")
.child_default("unrunnable")
.build(),
),
("system", component_decl_with_test_runner()),
("unrunnable", component_decl_with_test_runner()),
],
Moniker::root(),
)
.await;
test.mock_runner.cause_failure("unrunnable");
// Instance not found.
{
let child_ref = fdecl::ChildRef { name: "missing".to_string(), collection: None };
let (_, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let err = test
.realm_proxy
.open_exposed_dir(&child_ref, server_end)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceNotFound);
}
// Instance cannot resolve.
{
let child_ref = fdecl::ChildRef { name: "unresolvable".to_string(), collection: None };
let (_, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let err = test
.realm_proxy
.open_exposed_dir(&child_ref, server_end)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceCannotResolve);
}
// Instance can't run.
{
let child_ref = fdecl::ChildRef { name: "unrunnable".to_string(), collection: None };
let (dir_proxy, server_end) =
endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let res = test.realm_proxy.open_exposed_dir(&child_ref, server_end).await;
res.expect("fidl call failed").expect("open_exposed_dir() failed");
let echo_proxy = client::connect_to_named_protocol_at_dir_root::<echo::EchoMarker>(
&dir_proxy, "hippo",
)
.expect("failed to open hippo service");
let res = echo_proxy.echo_string(Some("hippos")).await;
assert!(res.is_err());
}
// Instance died.
{
test.drop_component();
let child_ref = fdecl::ChildRef { name: "system".to_string(), collection: None };
let (_, server_end) = endpoints::create_proxy::<fio::DirectoryMarker>().unwrap();
let err = test
.realm_proxy
.open_exposed_dir(&child_ref, server_end)
.await
.expect("fidl call failed")
.expect_err("unexpected success");
assert_eq!(err, fcomponent::Error::InstanceDied);
}
}
}