blob: 2eb03ed565b291c6e0d03f35b5308f130eec2c69 [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::{
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::ServerEnd, prelude::*},
fidl_fuchsia_component as fcomponent, fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys,
fuchsia_zircon as zx,
futures::lock::Mutex,
futures::StreamExt,
lazy_static::lazy_static,
moniker::{AbsoluteMoniker, AbsoluteMonikerBase, RelativeMoniker, RelativeMonikerBase},
std::{
convert::TryFrom,
path::PathBuf,
sync::{Arc, Weak},
},
tracing::warn,
};
lazy_static! {
pub static ref REALM_QUERY_CAPABILITY_NAME: CapabilityName =
fsys::RealmQueryMarker::PROTOCOL_NAME.into();
}
// Serves the fuchsia.sys2.RealmQuery protocol.
pub struct RealmQuery {
model: Arc<Model>,
}
impl RealmQuery {
pub fn new(model: Arc<Model>) -> Self {
Self { model }
}
pub fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> {
vec![HooksRegistration::new(
"RealmQuery",
vec![EventType::CapabilityRouted],
Arc::downgrade(self) as Weak<dyn Hook>,
)]
}
/// Given a `CapabilitySource`, determine if it is a framework-provided
/// RealmQuery 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_QUERY_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(RealmQueryCapabilityProvider::query(
self,
component.abs_moniker.clone(),
)));
}
}
}
Ok(())
}
/// Create the instance info and state matching the given moniker string in this scope
async fn get_instance_info_and_resolved_state(
self: &Arc<Self>,
scope_moniker: &AbsoluteMoniker,
moniker_str: String,
) -> Result<(fsys::InstanceInfo, Option<Box<fsys::ResolvedState>>), fcomponent::Error> {
// Construct the complete moniker using the scope moniker and the relative moniker string.
let moniker = join_monikers(scope_moniker, &moniker_str)?;
let instance =
self.model.find(&moniker).await.ok_or(fcomponent::Error::InstanceNotFound)?;
let resolved = instance.create_fidl_resolved_state().await;
let relative_moniker = extract_relative_moniker(scope_moniker, &moniker);
let instance_id = self.model.component_id_index().look_up_moniker(&moniker).cloned();
let state = match &resolved {
Some(r) => {
if r.started.is_some() {
fsys::InstanceState::Started
} else {
fsys::InstanceState::Resolved
}
}
None => fsys::InstanceState::Unresolved,
};
let info = fsys::InstanceInfo {
moniker: relative_moniker.to_string(),
url: instance.component_url.clone(),
instance_id,
state,
};
Ok((info, resolved))
}
/// Serve the fuchsia.sys2.RealmQuery protocol for a given scope on a given stream
async fn serve(
self: Arc<Self>,
scope_moniker: AbsoluteMoniker,
mut stream: fsys::RealmQueryRequestStream,
) {
loop {
let fsys::RealmQueryRequest::GetInstanceInfo { moniker, responder } =
match stream.next().await {
Some(Ok(request)) => request,
Some(Err(error)) => {
warn!(?error, "Could not get next RealmQuery request");
break;
}
None => break,
};
let mut result =
self.get_instance_info_and_resolved_state(&scope_moniker, moniker).await;
if let Err(error) = responder.send(&mut result) {
warn!(?error, "Could not respond to GetInstanceInfo request");
break;
}
}
}
}
#[async_trait]
impl Hook for RealmQuery {
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 RealmQueryCapabilityProvider {
query: Arc<RealmQuery>,
scope_moniker: AbsoluteMoniker,
}
impl RealmQueryCapabilityProvider {
pub fn query(query: Arc<RealmQuery>, scope_moniker: AbsoluteMoniker) -> Self {
Self { query, scope_moniker }
}
}
#[async_trait]
impl CapabilityProvider for RealmQueryCapabilityProvider {
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, "RealmQuery capability got open request with bad");
return Ok(());
}
if relative_path.components().count() != 0 {
warn!(
"RealmQuery 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::RealmQueryMarker>::new(server_end);
let stream: fsys::RealmQueryRequestStream =
server_end.into_stream().map_err(ModelError::stream_creation_error)?;
task_scope
.add_task(async move {
self.query.serve(self.scope_moniker, stream).await;
})
.await;
Ok(())
}
}
/// Takes the scoped component's moniker and a relative moniker string and join them into an
/// absolute moniker.
fn join_monikers(
scope_moniker: &AbsoluteMoniker,
moniker_str: &str,
) -> Result<AbsoluteMoniker, fcomponent::Error> {
let relative_moniker =
RelativeMoniker::try_from(moniker_str).map_err(|_| fcomponent::Error::InvalidArguments)?;
if !relative_moniker.up_path().is_empty() {
return Err(fcomponent::Error::InvalidArguments);
}
let abs_moniker = AbsoluteMoniker::from_relative(scope_moniker, &relative_moniker)
.map_err(|_| fcomponent::Error::InvalidArguments)?;
Ok(abs_moniker)
}
/// 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},
assert_matches::assert_matches,
cm_rust::*,
cm_rust_testing::ComponentDeclBuilder,
fidl::endpoints::create_proxy_and_stream,
fidl_fuchsia_component as fcomponent, fidl_fuchsia_component_config as fconfig,
fidl_fuchsia_component_decl as fdecl, fuchsia_async as fasync,
moniker::*,
routing_test_helpers::component_id_index::make_index_file,
};
#[fuchsia::test]
async fn read_all_properties() {
// 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 use_decl = UseDecl::Protocol(UseProtocolDecl {
source: UseSource::Framework,
source_name: "foo".into(),
target_path: CapabilityPath::try_from("/svc/foo").unwrap(),
dependency_type: DependencyType::Strong,
availability: Availability::Required,
});
let expose_decl = ExposeDecl::Protocol(ExposeProtocolDecl {
source: ExposeSource::Self_,
source_name: "bar".into(),
target: ExposeTarget::Parent,
target_name: "bar".into(),
});
let checksum = ConfigChecksum::Sha256([
0x07, 0xA8, 0xE6, 0x85, 0xC8, 0x79, 0xA9, 0x79, 0xC3, 0x26, 0x17, 0xDC, 0x4E, 0x74,
0x65, 0x7F, 0xF1, 0xF7, 0x73, 0xE7, 0x12, 0xEE, 0x51, 0xFD, 0xF6, 0x57, 0x43, 0x07,
0xA7, 0xAF, 0x2E, 0x64,
]);
let config = ConfigDecl {
fields: vec![ConfigField { key: "my_field".to_string(), type_: ConfigValueType::Bool }],
checksum: checksum.clone(),
value_source: ConfigValueSource::PackagePath("meta/root.cvf".into()),
};
let config_values = ValuesData {
values: vec![ValueSpec { value: Value::Single(SingleValue::Bool(true)) }],
checksum: checksum.clone(),
};
let components = vec![(
"root",
ComponentDeclBuilder::new()
.add_config(config)
.use_(use_decl.clone())
.expose(expose_decl.clone())
.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))
.set_config_values(vec![("meta/root.cvf", config_values)])
.build()
.await;
let realm_query = {
let env = builtin_environment.lock().await;
env.realm_query.clone().unwrap()
};
let (query, query_request_stream) =
create_proxy_and_stream::<fsys::RealmQueryMarker>().unwrap();
let _query_task = fasync::Task::local(async move {
realm_query.serve(AbsoluteMoniker::root(), query_request_stream).await
});
model.start().await;
let (info, resolved) = query.get_instance_info("./").await.unwrap().unwrap();
assert_eq!(info.moniker, ".");
assert_eq!(info.url, "test:///root");
assert_eq!(info.state, fsys::InstanceState::Started);
assert_eq!(info.instance_id.clone().unwrap(), iid);
let resolved = resolved.unwrap();
let started = resolved.started.unwrap();
// Component should have one config field with right value
let config = resolved.config.unwrap();
assert_eq!(config.fields.len(), 1);
let field = &config.fields[0];
assert_eq!(field.key, "my_field");
assert_matches!(field.value, fconfig::Value::Single(fconfig::SingleValue::Bool(true)));
assert_eq!(config.checksum, checksum.native_into_fidl());
// Component should have one use and one expose decl
assert_eq!(resolved.uses.len(), 1);
assert_eq!(resolved.uses[0], use_decl.native_into_fidl());
assert_eq!(resolved.exposes.len(), 1);
assert_eq!(resolved.exposes[0], expose_decl.native_into_fidl());
// Test resolvers provide a pkg dir with a fake file
let pkg_dir = resolved.pkg_dir.unwrap();
let pkg_dir = pkg_dir.into_proxy().unwrap();
let entries = fuchsia_fs::directory::readdir(&pkg_dir).await.unwrap();
assert_eq!(
entries,
vec![fuchsia_fs::directory::DirEntry {
name: "fake_file".to_string(),
kind: fuchsia_fs::directory::DirentKind::File
}]
);
// Component Manager serves the exposed dir with the `bar` protocol
let exposed_dir = resolved.exposed_dir.into_proxy().unwrap();
let entries = fuchsia_fs::directory::readdir(&exposed_dir).await.unwrap();
assert_eq!(
entries,
vec![fuchsia_fs::directory::DirEntry {
name: "bar".to_string(),
kind: fuchsia_fs::directory::DirentKind::Unknown
}]
);
// Component Manager serves the namespace dir with the `foo` protocol.
let ns_dir = resolved.ns_dir.into_proxy().unwrap();
let entries = fuchsia_fs::directory::readdir(&ns_dir).await.unwrap();
assert_eq!(
entries,
vec![
fuchsia_fs::directory::DirEntry {
name: "pkg".to_string(),
kind: fuchsia_fs::directory::DirentKind::Directory
},
fuchsia_fs::directory::DirEntry {
name: "svc".to_string(),
kind: fuchsia_fs::directory::DirentKind::Directory
},
]
);
let svc_dir =
fuchsia_fs::directory::open_directory(&ns_dir, "svc", fio::OpenFlags::RIGHT_READABLE)
.await
.unwrap();
let entries = fuchsia_fs::directory::readdir(&svc_dir).await.unwrap();
assert_eq!(
entries,
vec![fuchsia_fs::directory::DirEntry {
name: "foo".to_string(),
kind: fuchsia_fs::directory::DirentKind::Unknown
}]
);
// Test runners don't provide an out dir or a runtime dir
assert!(started.out_dir.is_none());
assert!(started.runtime_dir.is_none());
}
#[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_query = {
let env = builtin_environment.lock().await;
env.realm_query.clone().unwrap()
};
let (query, query_request_stream) =
create_proxy_and_stream::<fsys::RealmQueryMarker>().unwrap();
let _query_task = fasync::Task::local(async move {
realm_query.serve(AbsoluteMoniker::root(), query_request_stream).await
});
model.start().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, resolved) = query.get_instance_info("./my_coll:a").await.unwrap().unwrap();
assert_eq!(info.moniker, "./my_coll:a");
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Unresolved);
assert!(info.instance_id.is_none());
assert!(resolved.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, resolved) = query.get_instance_info("./my_coll:a").await.unwrap().unwrap();
assert_eq!(info.state, fsys::InstanceState::Resolved);
let resolved = resolved.unwrap();
assert!(resolved.config.is_none());
assert!(resolved.uses.is_empty());
assert!(resolved.exposes.is_empty());
assert!(resolved.pkg_dir.is_some());
assert!(resolved.started.is_none());
let result = component_a.start(&StartReason::Debug).await.unwrap();
assert_eq!(result, fsys::StartResult::Started);
// `a` should be started
let (info, resolved) = query.get_instance_info("./my_coll:a").await.unwrap().unwrap();
assert_eq!(info.state, fsys::InstanceState::Started);
let resolved = resolved.unwrap();
assert!(resolved.config.is_none());
assert!(resolved.uses.is_empty());
assert!(resolved.exposes.is_empty());
assert!(resolved.pkg_dir.is_some());
let started = resolved.started.unwrap();
assert!(started.out_dir.is_none());
assert!(started.runtime_dir.is_none());
component_a.stop_instance(false, false).await.unwrap();
// `a` should be stopped
let (info, resolved) = query.get_instance_info("./my_coll:a").await.unwrap().unwrap();
assert_eq!(info.state, fsys::InstanceState::Resolved);
let resolved = resolved.unwrap();
assert!(resolved.config.is_none());
assert!(resolved.uses.is_empty());
assert!(resolved.exposes.is_empty());
assert!(resolved.pkg_dir.is_some());
assert!(resolved.started.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
let err = query.get_instance_info("./my_coll:a").await.unwrap().unwrap_err();
assert_eq!(err, fcomponent::Error::InstanceNotFound);
}
#[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_query = {
let env = builtin_environment.lock().await;
env.realm_query.clone().unwrap()
};
let (query, query_request_stream) =
create_proxy_and_stream::<fsys::RealmQueryMarker>().unwrap();
let moniker_a = AbsoluteMoniker::parse_str("/a").unwrap();
let _query_task =
fasync::Task::local(
async move { realm_query.serve(moniker_a, query_request_stream).await },
);
model.start().await;
// `a` should be unresolved
let (info, resolved) = query.get_instance_info(".").await.unwrap().unwrap();
assert_eq!(info.moniker, ".");
assert_eq!(info.url, "test:///a");
assert_eq!(info.state, fsys::InstanceState::Unresolved);
assert!(info.instance_id.is_none());
assert!(resolved.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, resolved) = query.get_instance_info(".").await.unwrap().unwrap();
assert_eq!(info.state, fsys::InstanceState::Resolved);
let resolved = resolved.unwrap();
assert!(resolved.config.is_none());
assert!(resolved.uses.is_empty());
assert!(resolved.exposes.is_empty());
assert!(resolved.pkg_dir.is_some());
assert!(resolved.started.is_none());
let result = component_a.start(&StartReason::Debug).await.unwrap();
assert_eq!(result, fsys::StartResult::Started);
// `a` should be started
let (info, resolved) = query.get_instance_info(".").await.unwrap().unwrap();
assert_eq!(info.state, fsys::InstanceState::Started);
let resolved = resolved.unwrap();
assert!(resolved.config.is_none());
assert!(resolved.uses.is_empty());
assert!(resolved.exposes.is_empty());
assert!(resolved.pkg_dir.is_some());
let started = resolved.started.unwrap();
assert!(started.out_dir.is_none());
assert!(started.runtime_dir.is_none());
component_a.stop_instance(false, false).await.unwrap();
// `a` should be stopped
let (info, resolved) = query.get_instance_info(".").await.unwrap().unwrap();
assert_eq!(info.state, fsys::InstanceState::Resolved);
let resolved = resolved.unwrap();
assert!(resolved.config.is_none());
assert!(resolved.uses.is_empty());
assert!(resolved.exposes.is_empty());
assert!(resolved.pkg_dir.is_some());
assert!(resolved.started.is_none());
}
}