blob: c577ec01460f4a06c7e4a7fb155f19db3aa4db84 [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::model::{
actions::{start, ActionSet, StartAction},
error::ModelError,
model::Model,
realm::{BindReason, Realm},
},
async_trait::async_trait,
fidl_fuchsia_sys2 as fsys,
futures::future::{join_all, BoxFuture},
moniker::AbsoluteMoniker,
std::sync::{Arc, Weak},
};
/// A trait to enable support for different `bind()` implementations. This is used,
/// for example, for testing code that depends on `bind()`, but no other `Model`
/// functionality.
#[async_trait]
pub trait Binder: Send + Sync {
async fn bind<'a>(
&'a self,
abs_moniker: &'a AbsoluteMoniker,
reason: &'a BindReason,
) -> Result<Arc<Realm>, ModelError>;
}
#[async_trait]
impl Binder for Arc<Model> {
/// Binds to the component instance with the specified moniker. This has the following effects:
/// - Binds to the parent instance.
/// - Starts the component instance, if it is not already running and not shut down.
/// - Binds to any descendant component instances that need to be eagerly started.
// TODO: This function starts the parent component, but doesn't track the bindings anywhere.
// This means that when the child stops and the parent has no other reason to run, we won't
// stop the parent. To solve this, we need to track the bindings.
async fn bind<'a>(
&'a self,
abs_moniker: &'a AbsoluteMoniker,
reason: &'a BindReason,
) -> Result<Arc<Realm>, ModelError> {
bind_at_moniker(self, abs_moniker, reason).await
}
}
#[async_trait]
impl Binder for Weak<Model> {
async fn bind<'a>(
&'a self,
abs_moniker: &'a AbsoluteMoniker,
reason: &'a BindReason,
) -> Result<Arc<Realm>, ModelError> {
if let Some(model) = self.upgrade() {
model.bind(abs_moniker, reason).await
} else {
Err(ModelError::ModelNotAvailable)
}
}
}
/// Binds to the component instance in the given realm, starting it if it's not already running.
/// Returns the realm that was bound to.
pub async fn bind_at_moniker<'a>(
model: &'a Arc<Model>,
abs_moniker: &'a AbsoluteMoniker,
reason: &BindReason,
) -> Result<Arc<Realm>, ModelError> {
let mut cur_moniker = AbsoluteMoniker::root();
let mut realm = model.root_realm.clone();
bind_at(realm.clone(), reason).await?;
for m in abs_moniker.path().iter() {
cur_moniker = cur_moniker.child(m.clone());
realm = model.look_up_realm(&cur_moniker).await?;
bind_at(realm.clone(), reason).await?;
}
Ok(realm)
}
/// Binds to the component instance in the given realm, starting it if it's not already
/// running.
pub async fn bind_at(realm: Arc<Realm>, reason: &BindReason) -> Result<(), ModelError> {
// Skip starting a component instance that was already started.
// Eager binding can cause `bind_at` to be re-entrant. It's important to bail out here so
// we don't end up in an infinite loop of binding to the same eager child.
{
let execution = realm.lock_execution().await;
if let Some(res) = start::should_return_early(&execution, &realm.abs_moniker) {
return res;
}
}
ActionSet::register(realm.clone(), StartAction::new(reason.clone())).await?;
let eager_children: Vec<_> = {
let mut state = realm.lock_state().await;
let state = state.get_resolved_mut().expect("bind_single_instance: not resolved");
state
.live_child_realms()
.filter_map(|(_, r)| match r.startup {
fsys::StartupMode::Eager => Some(r.clone()),
fsys::StartupMode::Lazy => None,
})
.collect()
};
bind_eager_children_recursive(eager_children).await.or_else(|e| match e {
ModelError::InstanceShutDown { .. } => Ok(()),
_ => Err(e),
})?;
Ok(())
}
/// Binds to a list of instances, and any eager children they may return.
// This function recursive calls `bind_at`, so it returns a BoxFutuer,
fn bind_eager_children_recursive<'a>(
instances_to_bind: Vec<Arc<Realm>>,
) -> BoxFuture<'a, Result<(), ModelError>> {
let f = async move {
let futures: Vec<_> = instances_to_bind
.iter()
.map(|realm| async move { bind_at(realm.clone(), &BindReason::Eager).await })
.collect();
join_all(futures).await.into_iter().fold(Ok(()), |acc, r| acc.and_then(|_| r))?;
Ok(())
};
Box::pin(f)
}
#[cfg(test)]
mod tests {
use super::*;
use {
crate::{
builtin_environment::BuiltinEnvironment,
config::RuntimeConfig,
model::{
actions::{ActionKey, ActionSet},
events::{event::EventMode, registry::EventSubscription},
hooks::{EventPayload, EventType, HooksRegistration},
testing::{mocks::*, out_dir::OutDir, test_helpers::*, test_hook::TestHook},
},
},
cm_rust::{
CapabilityPath, ComponentDecl, RegistrationSource, RunnerDecl, RunnerRegistration,
RunnerSource,
},
fidl_fuchsia_component_runner as fcrunner, fuchsia_async as fasync,
futures::{join, prelude::*},
matches::assert_matches,
moniker::PartialMoniker,
std::{collections::HashSet, convert::TryFrom},
};
async fn new_model(
components: Vec<(&'static str, ComponentDecl)>,
) -> (Arc<Model>, Arc<BuiltinEnvironment>, Arc<MockRunner>) {
new_model_with(components, vec![]).await
}
async fn new_model_with(
components: Vec<(&'static str, ComponentDecl)>,
additional_hooks: Vec<HooksRegistration>,
) -> (Arc<Model>, Arc<BuiltinEnvironment>, Arc<MockRunner>) {
let TestModelResult { model, builtin_environment, mock_runner, .. } =
new_test_model("root", components, RuntimeConfig::default()).await;
model.root_realm.hooks.install(additional_hooks).await;
(model, builtin_environment, mock_runner)
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_root() {
let (model, _builtin_environment, mock_runner) =
new_model(vec![("root", component_decl_with_test_runner())]).await;
let m: AbsoluteMoniker = AbsoluteMoniker::root();
let res = model.bind(&m, &BindReason::Root).await;
assert!(res.is_ok());
mock_runner.wait_for_url("test:///root_resolved").await;
let actual_children = get_live_children(&model.root_realm).await;
assert!(actual_children.is_empty());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_root_non_existent() {
let (model, _builtin_environment, mock_runner) =
new_model(vec![("root", component_decl_with_test_runner())]).await;
let m: AbsoluteMoniker = vec!["no-such-instance:0"].into();
let res = model.bind(&m, &BindReason::Root).await;
let expected_res: Result<Arc<Realm>, ModelError> =
Err(ModelError::instance_not_found(vec!["no-such-instance:0"].into()));
assert_eq!(format!("{:?}", res), format!("{:?}", expected_res));
mock_runner.wait_for_url("test:///root_resolved").await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_concurrent() {
// Test binding twice concurrently to the same component. The component should only be
// started once.
let (model, builtin_environment, mock_runner) = new_model(vec![
("root", ComponentDeclBuilder::new().add_lazy_child("system").build()),
("system", component_decl_with_test_runner()),
])
.await;
let events = vec![EventSubscription::new(EventType::Started.into(), EventMode::Sync)];
let mut event_source = builtin_environment
.event_source_factory
.create_for_debug(EventMode::Sync)
.await
.expect("create event source");
let mut event_stream =
event_source.subscribe(events).await.expect("subscribe to event stream");
event_source.start_component_tree().await;
// Bind to "system", pausing before it starts.
let model_copy = model.clone();
let (f, bind_handle) = async move {
let m: AbsoluteMoniker = vec!["system:0"].into();
model_copy.bind(&m, &BindReason::Root).await.expect("failed to bind 1");
}
.remote_handle();
fasync::Task::spawn(f).detach();
let event = event_stream.wait_until(EventType::Started, vec![].into()).await.unwrap();
event.resume();
let event =
event_stream.wait_until(EventType::Started, vec!["system:0"].into()).await.unwrap();
// Verify that the correct BindReason propagates to the event.
assert_matches!(
event.event.result,
Ok(EventPayload::Started { bind_reason: BindReason::Root, .. })
);
mock_runner.wait_for_url("test:///root_resolved").await;
// While the bind() is paused, simulate a second bind by explicitly scheduling a Start
// action. Allow the original bind to proceed, then check the result of both bindings.
let m: AbsoluteMoniker = vec!["system:0"].into();
let realm = model.look_up_realm(&m).await.expect("failed realm lookup");
let f = ActionSet::register(realm, StartAction::new(BindReason::Eager));
let (f, action_handle) = f.remote_handle();
fasync::Task::spawn(f).detach();
event.resume();
bind_handle.await;
action_handle.await.expect("failed to bind 2");
// Verify that the component was started only once.
mock_runner.wait_for_urls(&["test:///root_resolved", "test:///system_resolved"]).await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_parent_then_child() {
let hook = Arc::new(TestHook::new());
let (model, _builtin_environment, mock_runner) = new_model_with(
vec![
(
"root",
ComponentDeclBuilder::new()
.add_lazy_child("system")
.add_lazy_child("echo")
.build(),
),
("system", component_decl_with_test_runner()),
("echo", component_decl_with_test_runner()),
],
hook.hooks(),
)
.await;
// bind to system
let m: AbsoluteMoniker = vec!["system:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner.wait_for_urls(&["test:///root_resolved", "test:///system_resolved"]).await;
// Validate children. system is resolved, but not echo.
let actual_children = get_live_children(&*model.root_realm).await;
let mut expected_children: HashSet<PartialMoniker> = HashSet::new();
expected_children.insert("system".into());
expected_children.insert("echo".into());
assert_eq!(actual_children, expected_children);
let system_realm = get_live_child(&*model.root_realm, "system").await;
let echo_realm = get_live_child(&*model.root_realm, "echo").await;
let actual_children = get_live_children(&*system_realm).await;
assert!(actual_children.is_empty());
assert!(echo_realm.lock_state().await.get_resolved().is_none());
// bind to echo
let m: AbsoluteMoniker = vec!["echo:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner
.wait_for_urls(&[
"test:///root_resolved",
"test:///system_resolved",
"test:///echo_resolved",
])
.await;
// Validate children. Now echo is resolved.
let echo_realm = get_live_child(&*model.root_realm, "echo").await;
let actual_children = get_live_children(&*echo_realm).await;
assert!(actual_children.is_empty());
// Verify that the component topology matches expectations.
assert_eq!("(echo,system)", hook.print());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_child_binds_parent() {
let hook = Arc::new(TestHook::new());
let (model, _builtin_environment, mock_runner) = new_model_with(
vec![
("root", ComponentDeclBuilder::new().add_lazy_child("system").build()),
(
"system",
ComponentDeclBuilder::new()
.add_lazy_child("logger")
.add_lazy_child("netstack")
.build(),
),
("logger", component_decl_with_test_runner()),
("netstack", component_decl_with_test_runner()),
],
hook.hooks(),
)
.await;
// Bind to logger (before ever binding to system). Ancestors are bound first.
let m: AbsoluteMoniker = vec!["system:0", "logger:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner
.wait_for_urls(&[
"test:///root_resolved",
"test:///system_resolved",
"test:///logger_resolved",
])
.await;
// Bind to netstack.
let m: AbsoluteMoniker = vec!["system:0", "netstack:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner
.wait_for_urls(&[
"test:///root_resolved",
"test:///system_resolved",
"test:///logger_resolved",
"test:///netstack_resolved",
])
.await;
// finally, bind to system. Was already bound, so no new results.
let m: AbsoluteMoniker = vec!["system:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner
.wait_for_urls(&[
"test:///root_resolved",
"test:///system_resolved",
"test:///logger_resolved",
"test:///netstack_resolved",
])
.await;
// validate the component topology.
assert_eq!("(system(logger,netstack))", hook.print());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_child_non_existent() {
let (model, _builtin_environment, mock_runner) = new_model(vec![
("root", ComponentDeclBuilder::new().add_lazy_child("system").build()),
("system", component_decl_with_test_runner()),
])
.await;
// bind to system
let m: AbsoluteMoniker = vec!["system:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner.wait_for_urls(&["test:///root_resolved", "test:///system_resolved"]).await;
// can't bind to logger: it does not exist
let m: AbsoluteMoniker = vec!["system:0", "logger:0"].into();
let res = model.bind(&m, &BindReason::Root).await;
let expected_res: Result<(), ModelError> = Err(ModelError::instance_not_found(m));
assert_eq!(format!("{:?}", res), format!("{:?}", expected_res));
mock_runner.wait_for_urls(&["test:///root_resolved", "test:///system_resolved"]).await;
}
/// Create a hierarchy of children:
///
/// a
/// / \
/// b c
/// \
/// d
/// \
/// e
///
/// `b`, `c`, and `d` are started eagerly. `a` and `e` are lazy.
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_eager_children() {
let hook = Arc::new(TestHook::new());
let (model, _builtin_environment, mock_runner) = new_model_with(
vec![
("root", ComponentDeclBuilder::new().add_lazy_child("a").build()),
(
"a",
ComponentDeclBuilder::new().add_eager_child("b").add_eager_child("c").build(),
),
("b", component_decl_with_test_runner()),
("c", ComponentDeclBuilder::new().add_eager_child("d").build()),
("d", ComponentDeclBuilder::new().add_lazy_child("e").build()),
("e", component_decl_with_test_runner()),
],
hook.hooks(),
)
.await;
// Bind to the top component, and check that it and the eager components were started.
{
let m = AbsoluteMoniker::new(vec!["a:0".into()]);
let res = model.bind(&m, &BindReason::Root).await;
assert!(res.is_ok());
mock_runner
.wait_for_urls(&[
"test:///root_resolved",
"test:///a_resolved",
"test:///b_resolved",
"test:///c_resolved",
"test:///d_resolved",
])
.await;
}
// Verify that the component topology matches expectations.
assert_eq!("(a(b,c(d(e))))", hook.print());
}
/// `b` is an eager child of `a` that uses a runner provided by `a`. In the process of binding
/// to `a`, `b` will be eagerly started, which requires re-binding to `a`. This should work
/// without causing reentrance issues.
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_eager_children_reentrant() {
let hook = Arc::new(TestHook::new());
let (model, _builtin_environment, mock_runner) = new_model_with(
vec![
("root", ComponentDeclBuilder::new().add_lazy_child("a").build()),
(
"a",
ComponentDeclBuilder::new()
.add_child(
ChildDeclBuilder::new()
.name("b")
.url("test:///b")
.startup(fsys::StartupMode::Eager)
.environment("env")
.build(),
)
.runner(RunnerDecl {
name: "foo".into(),
source: RunnerSource::Self_,
source_path: CapabilityPath::try_from("/svc/runner").unwrap(),
})
.add_environment(
EnvironmentDeclBuilder::new()
.extends(fsys::EnvironmentExtends::Realm)
.name("env")
.add_runner(RunnerRegistration {
source_name: "foo".into(),
source: RegistrationSource::Self_,
target_name: "foo".into(),
})
.build(),
)
.build(),
),
("b", ComponentDeclBuilder::new_empty_component().use_runner("foo").build()),
],
hook.hooks(),
)
.await;
// Set up the runner.
let (runner_service, mut receiver) =
create_service_directory_entry::<fcrunner::ComponentRunnerMarker>();
let mut out_dir = OutDir::new();
out_dir.add_entry(CapabilityPath::try_from("/svc/runner").unwrap(), runner_service);
mock_runner.add_host_fn("test:///a_resolved", out_dir.host_fn());
// Bind to the top component, and check that it and the eager components were started.
{
let (f, bind_handle) = async move {
let m = AbsoluteMoniker::new(vec!["a:0".into()]);
model.bind(&m, &BindReason::Root).await
}
.remote_handle();
fasync::Task::spawn(f).detach();
// `b` uses the runner offered by `a`.
assert_eq!(
wait_for_runner_request(&mut receiver).await.resolved_url,
Some("test:///b_resolved".to_string())
);
bind_handle.await.expect("bind to `a` failed");
// `root` and `a` use the test runner.
mock_runner.wait_for_urls(&["test:///root_resolved", "test:///a_resolved"]).await;
}
// Verify that the component topology matches expectations.
assert_eq!("(a(b))", hook.print());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_no_execute() {
// Create a non-executable component with an eagerly-started child.
let (model, _builtin_environment, mock_runner) = new_model(vec![
("root", ComponentDeclBuilder::new().add_lazy_child("a").build()),
("a", ComponentDeclBuilder::new_empty_component().add_eager_child("b").build()),
("b", component_decl_with_test_runner()),
])
.await;
// Bind to the parent component. The child should be started. However, the parent component
// is non-executable so it is not run.
let m: AbsoluteMoniker = vec!["a:0"].into();
assert!(model.bind(&m, &BindReason::Root).await.is_ok());
mock_runner.wait_for_urls(&["test:///root_resolved", "test:///b_resolved"]).await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bind_action_sequence() {
// Test that binding registers the expected actions in the expected sequence
// (Discover -> Resolve -> Start).
// Set up the tree.
let (model, builtin_environment, _mock_runner) = new_model(vec![
("root", ComponentDeclBuilder::new().add_lazy_child("system").build()),
("system", component_decl_with_test_runner()),
])
.await;
let events = vec![
EventType::Discovered.into(),
EventType::Resolved.into(),
EventType::Started.into(),
];
let mut event_source = builtin_environment
.event_source_factory
.create_for_debug(EventMode::Sync)
.await
.expect("create event source");
let mut event_stream = event_source
.subscribe(
events
.into_iter()
.map(|event| EventSubscription::new(event, EventMode::Sync))
.collect(),
)
.await
.expect("subscribe to event stream");
event_source.start_component_tree().await;
// Child of root should start out discovered but not resolved yet.
let m = AbsoluteMoniker::new(vec!["system:0".into()]);
let start_model = model.start();
let check_events = async {
let event = event_stream.wait_until(EventType::Discovered, m.clone()).await.unwrap();
{
let root_state = model.root_realm.lock_state().await;
let root_state = root_state.get_resolved().unwrap();
let realm = root_state.get_child_instance(&"system:0".into()).unwrap();
let actions = realm.lock_actions().await;
assert!(actions.contains(&ActionKey::Discover));
assert!(!actions.contains(&ActionKey::Resolve));
}
event.resume();
let event = event_stream.wait_until(EventType::Started, vec![].into()).await.unwrap();
event.resume();
};
join!(start_model, check_events);
// Bind to child and check that it gets resolved, with a Resolve event and action.
let bind = async {
model.bind(&m, &BindReason::Root).await.unwrap();
};
let check_events = async {
let event = event_stream.wait_until(EventType::Resolved, m.clone()).await.unwrap();
// While the Resolved hook is handled, it should be possible to look up the realm
// without deadlocking.
let realm = model.look_up_realm(&m).await.unwrap();
{
let actions = realm.lock_actions().await;
assert!(actions.contains(&ActionKey::Resolve));
assert!(!actions.contains(&ActionKey::Discover));
}
event.resume();
// Check that the child is started, with a Start event and action.
let event = event_stream.wait_until(EventType::Started, m.clone()).await.unwrap();
{
let actions = realm.lock_actions().await;
assert!(actions.contains(&ActionKey::Start));
assert!(!actions.contains(&ActionKey::Discover));
assert!(!actions.contains(&ActionKey::Resolve));
}
event.resume();
};
join!(bind, check_events);
}
}