blob: dcdedff470faefc39d30161a56c5eb97c025fe5d [file] [log] [blame]
// Copyright 2020 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::{
error::ModelError,
events::{filter::EventFilter, synthesizer::EventSynthesisProvider},
hooks::{
Event, EventError, EventErrorPayload, EventPayload, EventType, Hook, HooksRegistration,
},
model::Model,
realm::Realm,
rights::{Rights, WRITE_RIGHTS},
},
async_trait::async_trait,
cm_rust::{
CapabilityName, CapabilityPath, ComponentDecl, ExposeDecl, ExposeDirectoryDecl,
ExposeProtocolDecl, ExposeSource, ExposeTarget,
},
fidl::endpoints::{Proxy, ServerEnd},
fidl_fuchsia_io::{self as fio, DirectoryProxy, NodeEvent, NodeMarker, NodeProxy},
fuchsia_async as fasync, fuchsia_zircon as zx,
futures::stream::StreamExt,
io_util,
log::*,
moniker::AbsoluteMoniker,
std::sync::{Arc, Weak},
};
/// Awaits for `Started` events and for each capability exposed to framework, dispatches a
/// `CapabilityReady` event.
pub struct CapabilityReadyNotifier {
model: Weak<Model>,
}
impl CapabilityReadyNotifier {
pub fn new(model: Weak<Model>) -> Self {
Self { model }
}
pub fn hooks(self: &Arc<Self>) -> Vec<HooksRegistration> {
vec![HooksRegistration::new(
"CapabilityReadyNotifier",
vec![EventType::Started],
Arc::downgrade(self) as Weak<dyn Hook>,
)]
}
async fn on_component_started(
self: &Arc<Self>,
target_moniker: &AbsoluteMoniker,
outgoing_dir: &DirectoryProxy,
decl: ComponentDecl,
) -> Result<(), ModelError> {
// Forward along errors into the new task so that dispatch can forward the
// error as an event.
let outgoing_node_result = clone_outgoing_root(&outgoing_dir, &target_moniker).await;
// Don't block the handling on the event on the exposed capabilities being ready
let this = self.clone();
let moniker = target_moniker.clone();
fasync::Task::spawn(async move {
// If we can't find the realm then we can't dispatch any CapabilityReady event,
// error or otherwise. This isn't necessarily an error as the model or realm might've been
// destroyed in the intervening time, so we just exit early.
let target_realm = match this.model.upgrade() {
Some(model) => {
if let Ok(realm) = model.look_up_realm(&moniker).await {
realm
} else {
return;
}
}
None => return,
};
let matching_exposes = filter_matching_exposes(&decl, None);
this.dispatch_capabilities_ready(
outgoing_node_result,
&decl,
matching_exposes,
&target_realm,
)
.await;
})
.detach();
Ok(())
}
/// Waits for the OnOpen event on the directory. This will hang until the component starts
/// serving that directory. The directory should have been cloned/opened with DESCRIBE.
async fn wait_for_on_open(
&self,
node: &NodeProxy,
target_moniker: &AbsoluteMoniker,
path: String,
) -> Result<(), ModelError> {
let mut events = node.take_event_stream();
match events.next().await {
Some(Ok(NodeEvent::OnOpen_ { s: status, info: _ })) => zx::Status::ok(status)
.map_err(|_| ModelError::open_directory_error(target_moniker.clone(), path)),
_ => Err(ModelError::open_directory_error(target_moniker.clone(), path)),
}
}
/// Waits for the outgoing directory to be ready and then notifies hooks of all the capabilities
/// inside it that were exposed to the framework by the component.
async fn dispatch_capabilities_ready(
&self,
outgoing_node_result: Result<NodeProxy, ModelError>,
decl: &ComponentDecl,
matching_exposes: Vec<&ExposeDecl>,
target_realm: &Arc<Realm>,
) {
let capability_ready_events =
self.create_events(outgoing_node_result, decl, matching_exposes, target_realm).await;
for capability_ready_event in capability_ready_events {
target_realm.hooks.dispatch(&capability_ready_event).await.unwrap_or_else(|e| {
error!("Error notifying capability ready for {}: {:?}", target_realm.abs_moniker, e)
});
}
}
async fn create_events(
&self,
outgoing_node_result: Result<NodeProxy, ModelError>,
decl: &ComponentDecl,
matching_exposes: Vec<&ExposeDecl>,
target_realm: &Arc<Realm>,
) -> Vec<Event> {
// Forward along the result for opening the outgoing directory into the CapabilityReady
// dispatch in order to propagate any potential errors as an event.
let outgoing_dir_result = async move {
let outgoing_node = outgoing_node_result?;
self.wait_for_on_open(&outgoing_node, &target_realm.abs_moniker, "/".to_string())
.await?;
io_util::node_to_directory(outgoing_node).map_err(|_| {
ModelError::open_directory_error(target_realm.abs_moniker.clone(), "/")
})
}
.await;
let mut events = Vec::new();
for expose_decl in matching_exposes {
let event = match expose_decl {
ExposeDecl::Directory(ExposeDirectoryDecl { source_name, target_name, .. }) => {
let (source_path, rights) = {
if let Some(directory_decl) = decl.find_directory_source(source_name) {
(&directory_decl.source_path, directory_decl.rights)
} else {
panic!("Missing directory declaration for expose: {:?}", decl);
}
};
self.create_event(
&target_realm,
outgoing_dir_result.as_ref(),
fio::MODE_TYPE_DIRECTORY,
Rights::from(rights),
source_path,
target_name,
)
.await
}
ExposeDecl::Protocol(ExposeProtocolDecl { source_name, target_name, .. }) => {
let source_path = {
if let Some(protocol_decl) = decl.find_protocol_source(source_name) {
&protocol_decl.source_path
} else {
panic!("Missing protocol declaration for expose: {:?}", decl);
}
};
self.create_event(
&target_realm,
outgoing_dir_result.as_ref(),
fio::MODE_TYPE_SERVICE,
Rights::from(*WRITE_RIGHTS),
source_path,
target_name,
)
.await
}
_ => {
unreachable!("should have skipped above");
}
};
events.push(event);
}
events
}
/// Creates an event with the directory at the given `target_path` inside the provided
/// outgoing directory if the capability is available.
async fn create_event(
&self,
target_realm: &Arc<Realm>,
outgoing_dir_result: Result<&DirectoryProxy, &ModelError>,
mode: u32,
rights: Rights,
source_path: &CapabilityPath,
target_name: &CapabilityName,
) -> Event {
let target_name = target_name.to_string();
let node_result = async move {
// DirProxy.open fails on absolute paths.
let source_path = source_path.to_string();
let canonicalized_path = io_util::canonicalize_path(&source_path);
let outgoing_dir = outgoing_dir_result.map_err(|e| e.clone())?;
let (node, server_end) = fidl::endpoints::create_proxy::<NodeMarker>().unwrap();
outgoing_dir
.open(
rights.into_legacy() | fio::OPEN_FLAG_DESCRIBE,
mode,
&canonicalized_path,
ServerEnd::new(server_end.into_channel()),
)
.map_err(|_| {
ModelError::open_directory_error(
target_realm.abs_moniker.clone(),
source_path.clone(),
)
})?;
self.wait_for_on_open(&node, &target_realm.abs_moniker, canonicalized_path.to_string())
.await?;
Ok(node)
}
.await;
match node_result {
Ok(node) => Event::new(
&target_realm,
Ok(EventPayload::CapabilityReady { name: target_name, node }),
),
Err(e) => Event::new(
&target_realm,
Err(EventError::new(&e, EventErrorPayload::CapabilityReady { name: target_name })),
),
}
}
}
fn filter_matching_exposes<'a>(
decl: &'a ComponentDecl,
filter: Option<&EventFilter>,
) -> Vec<&'a ExposeDecl> {
decl.exposes
.iter()
.filter(|expose_decl| {
match expose_decl {
ExposeDecl::Directory(ExposeDirectoryDecl {
source, target, target_name, ..
})
| ExposeDecl::Protocol(ExposeProtocolDecl {
source, target, target_name, ..
}) => {
if let Some(filter) = filter {
if !filter.contains("name", vec![target_name.to_string()]) {
return false;
}
}
if target != &ExposeTarget::Framework || source != &ExposeSource::Self_ {
return false;
}
}
_ => {
return false;
}
}
true
})
.collect()
}
async fn clone_outgoing_root(
outgoing_dir: &DirectoryProxy,
target_moniker: &AbsoluteMoniker,
) -> Result<NodeProxy, ModelError> {
let outgoing_dir = io_util::clone_directory(
&outgoing_dir,
fio::CLONE_FLAG_SAME_RIGHTS | fio::OPEN_FLAG_DESCRIBE,
)
.map_err(|_| ModelError::open_directory_error(target_moniker.clone(), "/"))?;
let outgoing_dir_channel = outgoing_dir
.into_channel()
.map_err(|_| ModelError::open_directory_error(target_moniker.clone(), "/"))?;
Ok(NodeProxy::from_channel(outgoing_dir_channel))
}
#[async_trait]
impl EventSynthesisProvider for CapabilityReadyNotifier {
async fn provide(&self, realm: Arc<Realm>, filter: EventFilter) -> Vec<Event> {
let decl = {
if let Some(state) = realm.lock_state().await.get_resolved() {
state.decl().clone()
} else {
return Vec::new();
}
};
let matching_exposes = filter_matching_exposes(&decl, Some(&filter));
if matching_exposes.is_empty() {
// Short-circuit if there are no matching exposes so we don't wait for the component's
// outgoing directory if there are no CapabilityReady events to send.
return vec![];
}
let maybe_outgoing_node_result =
async {
let execution = realm.lock_execution().await;
if execution.runtime.is_none() {
return None;
}
let runtime = execution.runtime.as_ref().unwrap();
let out_dir = match runtime.outgoing_dir.as_ref().ok_or(
ModelError::open_directory_error(realm.abs_moniker.clone(), "/".to_string()),
) {
Ok(out_dir) => out_dir,
Err(e) => return Some(Err(e)),
};
Some(clone_outgoing_root(&out_dir, &realm.abs_moniker).await)
}
.await;
let outgoing_node_result = match maybe_outgoing_node_result {
None => return vec![],
Some(result) => result,
};
self.create_events(outgoing_node_result, &decl, matching_exposes, &realm).await
}
}
#[async_trait]
impl Hook for CapabilityReadyNotifier {
async fn on(self: Arc<Self>, event: &Event) -> Result<(), ModelError> {
match &event.result {
Ok(EventPayload::Started { runtime, component_decl, .. }) => {
if filter_matching_exposes(&component_decl, None).is_empty() {
// Short-circuit if there are no matching exposes so we don't spawn a task
// if there's nothing to do. In particular, don't wait for the component's
// outgoing directory if there are no CapabilityReady events to send.
return Ok(());
}
if let Some(outgoing_dir) = &runtime.outgoing_dir {
self.on_component_started(
&event.target_moniker,
outgoing_dir,
component_decl.clone(),
)
.await?;
}
}
_ => {}
}
Ok(())
}
}