blob: b86ed28fe34ebebd6e98e2f214a40df3e87c4a0e [file] [log] [blame]
// Copyright 2022 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::{
capability::{CapabilityProvider, CapabilitySource},
model::{
component::{ComponentInstance, InstanceState},
error::ModelError,
hooks::{Event, EventPayload, EventType, Hook, HooksRegistration},
model::Model,
},
},
async_trait::async_trait,
cm_rust::CapabilityName,
cm_task_scope::TaskScope,
cm_util::channel,
fidl::{
endpoints::{ClientEnd, ServerEnd},
prelude::*,
},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys,
fuchsia_async as fasync, fuchsia_zircon as zx,
fuchsia_zircon::sys::ZX_CHANNEL_MAX_MSG_BYTES,
futures::lock::Mutex,
futures::StreamExt,
lazy_static::lazy_static,
measure_tape_for_instance_info::Measurable,
moniker::{AbsoluteMoniker, AbsoluteMonikerBase, RelativeMoniker, RelativeMonikerBase},
std::{
path::PathBuf,
sync::{Arc, Weak},
},
tracing::warn,
};
lazy_static! {
pub static ref REALM_EXPLORER_CAPABILITY_NAME: CapabilityName =
fsys::RealmExplorerMarker::PROTOCOL_NAME.into();
}
// Number of bytes the header of a vector occupies in a fidl message.
// TODO(https://fxbug.dev/98653): This should be a constant in a FIDL library.
const FIDL_VECTOR_HEADER_BYTES: usize = 16;
// Number of bytes the header of a fidl message occupies.
// TODO(https://fxbug.dev/98653): This should be a constant in a FIDL library.
const FIDL_HEADER_BYTES: usize = 16;
// Serves the fuchsia.sys2.RealmExplorer protocols.
pub struct RealmExplorer {
model: Arc<Model>,
}
impl RealmExplorer {
pub fn new(model: Arc<Model>) -> Self {
Self { model }
}
pub fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> {
vec![HooksRegistration::new(
"RealmExplorer",
vec![EventType::CapabilityRouted],
Arc::downgrade(self) as Weak<dyn Hook>,
)]
}
/// Given a `CapabilitySource`, determine if it is a framework-provided
/// RealmExplorer capability. If so, serve the capability.
async fn on_capability_routed_async(
self: Arc<Self>,
source: CapabilitySource,
capability_provider: Arc<Mutex<Option<Box<dyn CapabilityProvider>>>>,
) -> Result<(), ModelError> {
// If this is a scoped framework directory capability, then check the source path
if let CapabilitySource::Framework { capability, component } = source {
if capability.matches_protocol(&REALM_EXPLORER_CAPABILITY_NAME) {
// Set the capability provider, if not already set.
let mut capability_provider = capability_provider.lock().await;
if capability_provider.is_none() {
*capability_provider = Some(Box::new(RealmExplorerCapabilityProvider::new(
self,
component.abs_moniker.clone(),
)));
}
}
}
Ok(())
}
/// Create the detailed instance info matching the given moniker string in this scope
/// and return all live children of the instance.
async fn get_instance_info_and_children(
self: &Arc<Self>,
scope_moniker: &AbsoluteMoniker,
instance: &Arc<ComponentInstance>,
) -> (fsys::InstanceInfo, Vec<Arc<ComponentInstance>>) {
let relative_moniker = extract_relative_moniker(scope_moniker, &instance.abs_moniker);
let instance_id =
self.model.component_id_index().look_up_moniker(&instance.abs_moniker).cloned();
let (state, children) = {
let state = instance.lock_state().await;
let execution = instance.lock_execution().await;
match &*state {
InstanceState::Resolved(r) => {
let children = r.children().map(|(_, c)| c.clone()).collect();
if execution.runtime.is_some() {
(fsys::InstanceState::Started, children)
} else {
(fsys::InstanceState::Resolved, children)
}
}
_ => (fsys::InstanceState::Unresolved, vec![]),
}
};
(
fsys::InstanceInfo {
moniker: relative_moniker.to_string(),
url: instance.component_url.clone(),
instance_id,
state,
},
children,
)
}
/// Take a snapshot of all instances in the given scope and create the instance info
/// FIDL object for each.
async fn snapshot_instance_infos(
self: &Arc<Self>,
scope_moniker: &AbsoluteMoniker,
) -> Result<Vec<fsys::InstanceInfo>, fcomponent::Error> {
let mut instance_infos = vec![];
// Only take instances contained within the scope realm
let scope_root =
self.model.find(scope_moniker).await.ok_or(fcomponent::Error::InstanceNotFound)?;
let mut queue = vec![scope_root];
while !queue.is_empty() {
let cur = queue.pop().unwrap();
let (instance_info, mut children) =
self.get_instance_info_and_children(scope_moniker, &cur).await;
instance_infos.push(instance_info);
queue.append(&mut children);
}
Ok(instance_infos)
}
/// Serve the fuchsia.sys2.RealmExplorer protocol for a given scope on a given stream
async fn serve(
self: Arc<Self>,
scope_moniker: AbsoluteMoniker,
mut stream: fsys::RealmExplorerRequestStream,
) {
loop {
let fsys::RealmExplorerRequest::GetAllInstanceInfos { responder } =
match stream.next().await {
Some(Ok(request)) => request,
Some(Err(error)) => {
warn!(?error, "Could not get next RealmExplorer request");
break;
}
None => break,
};
let mut result = match self.snapshot_instance_infos(&scope_moniker).await {
Ok(fidl_instances) => {
let client_end = serve_instance_info_iterator(fidl_instances);
Ok(client_end)
}
Err(e) => Err(e),
};
if let Err(error) = responder.send(&mut result) {
warn!(?error, "Could not respond to GetAllInstanceInfos request");
break;
}
}
}
}
fn serve_instance_info_iterator(
mut instance_infos: Vec<fsys::InstanceInfo>,
) -> ClientEnd<fsys::InstanceInfoIteratorMarker> {
let (client_end, server_end) =
fidl::endpoints::create_endpoints::<fsys::InstanceInfoIteratorMarker>().unwrap();
fasync::Task::spawn(async move {
let mut stream: fsys::InstanceInfoIteratorRequestStream = server_end.into_stream().unwrap();
while let Some(Ok(fsys::InstanceInfoIteratorRequest::Next { responder })) =
stream.next().await
{
let mut bytes_used: usize = FIDL_HEADER_BYTES + FIDL_VECTOR_HEADER_BYTES;
let mut instance_count = 0;
// Determine how many info objects can be sent in a single FIDL message.
// TODO(https://fxbug.dev/98653): This logic should be handled by FIDL.
for info in &instance_infos {
bytes_used += info.measure().num_bytes;
if bytes_used > ZX_CHANNEL_MAX_MSG_BYTES as usize {
break;
}
instance_count += 1;
}
let mut batch: Vec<fsys::InstanceInfo> =
instance_infos.drain(0..instance_count).collect();
let result = responder.send(&mut batch.iter_mut());
if let Err(error) = result {
warn!(?error, "RealmExplorer encountered error sending instance info batch");
break;
}
}
})
.detach();
client_end
}
#[async_trait]
impl Hook for RealmExplorer {
async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> {
match &event.result {
Ok(EventPayload::CapabilityRouted { source, capability_provider }) => {
self.on_capability_routed_async(source.clone(), capability_provider.clone())
.await?;
}
_ => {}
}
Ok(())
}
}
pub struct RealmExplorerCapabilityProvider {
explorer: Arc<RealmExplorer>,
scope_moniker: AbsoluteMoniker,
}
impl RealmExplorerCapabilityProvider {
pub fn new(explorer: Arc<RealmExplorer>, scope_moniker: AbsoluteMoniker) -> Self {
Self { explorer, scope_moniker }
}
}
#[async_trait]
impl CapabilityProvider for RealmExplorerCapabilityProvider {
async fn open(
self: Box<Self>,
task_scope: TaskScope,
flags: fio::OpenFlags,
_open_mode: u32,
relative_path: PathBuf,
server_end: &mut zx::Channel,
) -> Result<(), ModelError> {
if flags != fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE {
warn!(?flags, "RealmExplorer capability got open request with bad");
return Ok(());
}
if relative_path.components().count() != 0 {
warn!(
"RealmExplorer capability got open request with non-empty path: {}",
relative_path.display()
);
return Ok(());
}
let server_end = channel::take_channel(server_end);
let server_end = ServerEnd::<fsys::RealmExplorerMarker>::new(server_end);
let stream: fsys::RealmExplorerRequestStream =
server_end.into_stream().map_err(ModelError::stream_creation_error)?;
task_scope
.add_task(async move {
self.explorer.serve(self.scope_moniker, stream).await;
})
.await;
Ok(())
}
}
/// Takes a parent and child absolute moniker, strips out the parent portion from the child
/// and creates a relative moniker.
fn extract_relative_moniker(parent: &AbsoluteMoniker, child: &AbsoluteMoniker) -> RelativeMoniker {
assert!(parent.contains_in_realm(child));
let parent_len = parent.path().len();
let mut children = child.path().clone();
children.drain(0..parent_len);
RelativeMoniker::new(vec![], children)
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::model::component::StartReason,
crate::model::testing::test_helpers::{TestEnvironmentBuilder, TestModelResult},
cm_rust::*,
cm_rust_testing::ComponentDeclBuilder,
fidl::endpoints::create_proxy_and_stream,
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_decl as fdecl,
fuchsia_async as fasync,
moniker::*,
routing_test_helpers::component_id_index::make_index_file,
};
async fn get_instance_info(
explorer: &fsys::RealmExplorerProxy,
num_expected_instances: usize,
expected_moniker: &str,
) -> fsys::InstanceInfo {
let iterator = explorer.get_all_instance_infos().await.unwrap().unwrap();
let iterator = iterator.into_proxy().unwrap();
let mut instances = vec![];
loop {
let mut iteration = iterator.next().await.unwrap();
if iteration.is_empty() {
break;
}
instances.append(&mut iteration);
}
assert_eq!(instances.len(), num_expected_instances);
for instance in instances.drain(..) {
if instance.moniker == expected_moniker {
return instance;
}
}
panic!("Could not find instance matching moniker: {}", expected_moniker);
}
#[fuchsia::test]
async fn basic_test() {
// Create index.
let iid = format!("1234{}", "5".repeat(60));
let index_file = make_index_file(component_id_index::Index {
instances: vec![component_id_index::InstanceIdEntry {
instance_id: Some(iid.clone()),
appmgr_moniker: None,
moniker: Some(AbsoluteMoniker::parse_str("/").unwrap()),
}],
..component_id_index::Index::default()
})
.unwrap();
let components = vec![("root", ComponentDeclBuilder::new().build())];
let TestModelResult { model, builtin_environment, .. } = TestEnvironmentBuilder::new()
.set_components(components)
.set_component_id_index_path(index_file.path().to_str().map(str::to_string))
.build()
.await;
let realm_explorer = {
let env = builtin_environment.lock().await;
env.realm_explorer.clone().unwrap()
};
let (explorer, explorer_request_stream) =
create_proxy_and_stream::<fsys::RealmExplorerMarker>().unwrap();
let _explorer_task = fasync::Task::local(async move {
realm_explorer.serve(AbsoluteMoniker::root(), explorer_request_stream).await
});
model.start().await;
let info = get_instance_info(&explorer, 1, ".").await;
assert_eq!(info.url, "test:///root");
assert_eq!(info.state, fsys::InstanceState::Started);
assert_eq!(info.instance_id.clone().unwrap(), iid);
}
#[fuchsia::test]
async fn observe_dynamic_lifecycle() {
let components = vec![
(
"root",
ComponentDeclBuilder::new()
.add_collection(CollectionDecl {
name: "my_coll".to_string(),
durability: fdecl::Durability::Transient,
environment: None,
allowed_offers: cm_types::AllowedOffers::StaticOnly,
allow_long_names: false,
persistent_storage: None,
})
.build(),
),
("a", ComponentDeclBuilder::new().build()),
];
let TestModelResult { model, builtin_environment, .. } =
TestEnvironmentBuilder::new().set_components(components).build().await;
let realm_explorer = {
let env = builtin_environment.lock().await;
env.realm_explorer.clone().unwrap()
};
let (explorer, explorer_request_stream) =
create_proxy_and_stream::<fsys::RealmExplorerMarker>().unwrap();
let _explorer_task = fasync::Task::local(async move {
realm_explorer.serve(AbsoluteMoniker::root(), explorer_request_stream).await
});
model.start().await;
get_instance_info(&explorer, 1, ".").await;
let component_root = model.look_up(&AbsoluteMoniker::root()).await.unwrap();
component_root
.add_dynamic_child(
"my_coll".to_string(),
&ChildDecl {
name: "a".to_string(),
url: "test:///a".to_string(),
startup: fdecl::StartupMode::Lazy,
on_terminate: None,
environment: None,
},
fcomponent::CreateChildArgs::EMPTY,
)
.await
.unwrap();
// `a` should be unresolved
let info = get_instance_info(&explorer, 2, "./my_coll:a").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Unresolved);
assert!(info.instance_id.is_none());
let moniker_a = AbsoluteMoniker::parse_str("/my_coll:a").unwrap();
let component_a = model.look_up(&moniker_a).await.unwrap();
// `a` should be resolved
let info = get_instance_info(&explorer, 2, "./my_coll:a").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Resolved);
assert!(info.instance_id.is_none());
let result = component_a.start(&StartReason::Debug).await.unwrap();
assert_eq!(result, fsys::StartResult::Started);
// `a` should be started
let info = get_instance_info(&explorer, 2, "./my_coll:a").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Started);
assert!(info.instance_id.is_none());
component_a.stop_instance(false, false).await.unwrap();
// `a` should be stopped
let info = get_instance_info(&explorer, 2, "./my_coll:a").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Resolved);
assert!(info.instance_id.is_none());
let child_moniker = ChildMoniker::parse("my_coll:a").unwrap();
component_root.remove_dynamic_child(&child_moniker).await.unwrap();
// `a` should be destroyed after purge
get_instance_info(&explorer, 1, ".").await;
}
#[fuchsia::test]
async fn scoped_to_child() {
let components = vec![
("root", ComponentDeclBuilder::new().add_lazy_child("a").build()),
("a", ComponentDeclBuilder::new().build()),
];
let TestModelResult { model, builtin_environment, .. } =
TestEnvironmentBuilder::new().set_components(components).build().await;
let realm_explorer = {
let env = builtin_environment.lock().await;
env.realm_explorer.clone().unwrap()
};
let (explorer, explorer_request_stream) =
create_proxy_and_stream::<fsys::RealmExplorerMarker>().unwrap();
let moniker_a = AbsoluteMoniker::parse_str("/a").unwrap();
let _explorer_task = fasync::Task::local(async move {
realm_explorer.serve(moniker_a, explorer_request_stream).await
});
model.start().await;
// `a` should be unresolved
let info = get_instance_info(&explorer, 1, ".").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Unresolved);
assert!(info.instance_id.is_none());
let moniker_a = AbsoluteMoniker::parse_str("/a").unwrap();
let component_a = model.look_up(&moniker_a).await.unwrap();
// `a` should be resolved
let info = get_instance_info(&explorer, 1, ".").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Resolved);
assert!(info.instance_id.is_none());
let result = component_a.start(&StartReason::Debug).await.unwrap();
assert_eq!(result, fsys::StartResult::Started);
// `a` should be started
let info = get_instance_info(&explorer, 1, ".").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Started);
assert!(info.instance_id.is_none());
component_a.stop_instance(false, false).await.unwrap();
// `a` should be stopped
let info = get_instance_info(&explorer, 1, ".").await;
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Resolved);
assert!(info.instance_id.is_none());
}
}