blob: 29334baf3159d3c71ec03354de867f6d209d3f7b [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 {
super::{
data::{self, Data, LazyNode},
metrics::Metrics,
PUPPET_MONIKER,
},
anyhow::{format_err, Error},
fidl_diagnostics_validate as validate, fidl_fuchsia_inspect as fidl_inspect,
fuchsia_component::client as fclient,
fuchsia_zircon::{self as zx, Vmo},
serde::Serialize,
};
pub const VMO_SIZE: u64 = 4096;
#[derive(Debug)]
pub struct Config {
pub diff_type: DiffType,
pub printable_name: String,
pub has_runner_node: bool,
pub test_archive: bool,
}
/// When reporting a discrepancy between local and remote Data trees, should the output include:
/// - The full rendering of both trees?
/// - The condensed diff between the trees? (This may still be quite large.)
/// - Both full and condensed renderings?
#[derive(Clone, Copy, Debug, Serialize)]
pub enum DiffType {
Full,
Diff,
Both,
}
impl From<Option<validate::DiffType>> for DiffType {
fn from(original: Option<validate::DiffType>) -> Self {
match original {
Some(validate::DiffType::Diff) => Self::Diff,
Some(validate::DiffType::Both) => Self::Both,
_ => Self::Full,
}
}
}
pub struct Puppet {
pub vmo: Vmo,
// Need to remember the connection to avoid dropping the VMO
connection: Connection,
// A printable name for output to the user.
pub config: Config,
}
impl Puppet {
pub async fn apply(
&mut self,
action: &mut validate::Action,
) -> Result<validate::TestResult, Error> {
Ok(self.connection.fidl.act(action).await?)
}
pub async fn apply_lazy(
&mut self,
lazy_action: &mut validate::LazyAction,
) -> Result<validate::TestResult, Error> {
match &self.connection.root_link_channel {
Some(_) => Ok(self.connection.fidl.act_lazy(lazy_action).await?),
None => Ok(validate::TestResult::Unimplemented),
}
}
pub async fn publish(&mut self) -> Result<validate::TestResult, Error> {
Ok(self.connection.fidl.publish().await?)
}
pub async fn connect() -> Result<Self, Error> {
Puppet::initialize_with_connection(Connection::connect().await?).await
}
pub(crate) async fn shutdown(self) {
let lifecycle_controller =
fclient::connect_to_protocol::<fidl_fuchsia_sys2::LifecycleControllerMarker>().unwrap();
lifecycle_controller
.stop_instance(&format!("./{}", PUPPET_MONIKER))
.await
.unwrap()
.unwrap();
}
/// Get the printable name associated with this puppet/test
pub fn printable_name(&self) -> &str {
&self.config.printable_name
}
#[cfg(test)]
pub async fn connect_local(local_fidl: validate::InspectPuppetProxy) -> Result<Puppet, Error> {
let mut puppet = Puppet::initialize_with_connection(Connection::new(local_fidl)).await?;
puppet.config.test_archive = false;
Ok(puppet)
}
async fn initialize_with_connection(mut connection: Connection) -> Result<Puppet, Error> {
Ok(Puppet {
vmo: connection.initialize_vmo().await?,
config: connection.get_config().await?,
connection,
})
}
pub async fn read_data(&self) -> Result<Data, Error> {
Ok(match &self.connection.root_link_channel {
None => data::Scanner::try_from(&self.vmo)?.data(),
Some(root_link_channel) => {
let vmo_tree = LazyNode::new(root_link_channel.clone()).await?;
data::Scanner::try_from(vmo_tree)?.data()
}
})
}
pub fn metrics(&self) -> Result<Metrics, Error> {
Ok(data::Scanner::try_from(&self.vmo)?.metrics())
}
}
struct Connection {
fidl: validate::InspectPuppetProxy,
// Connection to Tree FIDL if Puppet supports it.
// Puppets can add support by implementing InitializeTree method.
root_link_channel: Option<fidl_inspect::TreeProxy>,
}
impl Connection {
async fn connect() -> Result<Self, Error> {
let puppet_fidl = fclient::connect_to_protocol::<validate::InspectPuppetMarker>().unwrap();
Ok(Self::new(puppet_fidl))
}
async fn get_config(&self) -> Result<Config, Error> {
let (printable_name, opts) = self.fidl.get_config().await?;
Ok(Config {
diff_type: opts.diff_type.into(),
printable_name,
has_runner_node: opts.has_runner_node.unwrap_or(false),
test_archive: true,
})
}
async fn fetch_link_channel(
fidl: &validate::InspectPuppetProxy,
) -> Option<fidl_inspect::TreeProxy> {
let params =
validate::InitializationParams { vmo_size: Some(VMO_SIZE), ..Default::default() };
let response = fidl.initialize_tree(&params).await;
if let Ok((Some(tree_client_end), validate::TestResult::Ok)) = response {
tree_client_end.into_proxy().ok()
} else {
None
}
}
async fn get_vmo_handle(channel: &fidl_inspect::TreeProxy) -> Result<Vmo, Error> {
let tree_content = channel.get_content().await?;
let buffer = tree_content.buffer.ok_or(format_err!("Buffer doesn't contain VMO"))?;
Ok(buffer.vmo)
}
fn new(fidl: validate::InspectPuppetProxy) -> Self {
Self { fidl, root_link_channel: None }
}
async fn initialize_vmo(&mut self) -> Result<Vmo, Error> {
self.root_link_channel = Self::fetch_link_channel(&self.fidl).await;
match &self.root_link_channel {
Some(root_link_channel) => Self::get_vmo_handle(&root_link_channel).await,
None => {
let params = validate::InitializationParams {
vmo_size: Some(VMO_SIZE),
..Default::default()
};
let handle: Option<zx::Handle>;
let out = self.fidl.initialize(&params).await?;
if let (Some(out_handle), _) = out {
handle = Some(out_handle);
} else {
return Err(format_err!("Didn't get a VMO handle"));
}
match handle {
Some(unwrapped_handle) => Ok(Vmo::from(unwrapped_handle)),
None => {
return Err(format_err!("Failed to unwrap handle"));
}
}
}
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use {
super::*,
crate::create_node,
anyhow::Context as _,
fidl::endpoints::{create_proxy, RequestStream, ServerEnd},
fidl_diagnostics_validate::{
Action, CreateNode, CreateNumericProperty, InspectPuppetMarker, InspectPuppetRequest,
InspectPuppetRequestStream, Options, TestResult, Value, ROOT_ID,
},
fuchsia_async as fasync,
fuchsia_inspect::{Inspector, InspectorConfig, IntProperty, Node},
fuchsia_zircon::HandleBased,
futures::prelude::*,
std::collections::HashMap,
tracing::info,
};
#[fuchsia::test]
async fn test_fidl_loopback() -> Result<(), Error> {
let mut puppet = local_incomplete_puppet().await?;
assert_eq!(puppet.vmo.get_size().unwrap(), VMO_SIZE);
let tree = puppet.read_data().await?;
assert_eq!(tree.to_string(), "root ->".to_string());
let mut data = Data::new();
tree.compare(&data, DiffType::Full)?;
let mut action = create_node!(parent: ROOT_ID, id: 1, name: "child");
puppet.apply(&mut action).await?;
data.apply(&action)?;
let tree = data::Scanner::try_from(&puppet.vmo)?.data();
assert_eq!(tree.to_string(), "root ->\n> child ->".to_string());
tree.compare(&data, DiffType::Full)?;
Ok(())
}
// This is a partial implementation.
// All it can do is initialize, and then create nodes and int properties (which it
// will hold forever). Trying to create a uint property will return Unimplemented.
// Other actions will give various kinds of incorrect results.
pub(crate) async fn local_incomplete_puppet() -> Result<Puppet, Error> {
let (client_end, server_end) = create_proxy().unwrap();
spawn_local_puppet(server_end).await;
Ok(Puppet::connect_local(client_end).await?)
}
async fn spawn_local_puppet(server_end: ServerEnd<InspectPuppetMarker>) {
fasync::Task::spawn(
async move {
// Inspector must be remembered so its VMO persists
let mut inspector_maybe: Option<Inspector> = None;
let mut nodes: HashMap<u32, Node> = HashMap::new();
let mut properties: HashMap<u32, IntProperty> = HashMap::new();
let server_chan = fasync::Channel::from_channel(server_end.into_channel());
let mut stream = InspectPuppetRequestStream::from_channel(server_chan);
while let Some(event) = stream.try_next().await? {
match event {
InspectPuppetRequest::GetConfig { responder } => {
responder.send("*Local*", Options::default()).ok();
}
InspectPuppetRequest::Initialize { params, responder } => {
let inspector = match params.vmo_size {
Some(size) => {
Inspector::new(InspectorConfig::default().size(size as usize))
}
None => Inspector::default(),
};
responder
.send(
inspector.duplicate_vmo().map(|v| v.into_handle()),
TestResult::Ok,
)
.context("responding to initialize")?;
inspector_maybe = Some(inspector);
}
InspectPuppetRequest::Act { action, responder } => match action {
Action::CreateNode(CreateNode { parent, id, name }) => {
inspector_maybe.as_ref().map(|i| {
let parent_node = if parent == ROOT_ID {
i.root()
} else {
nodes.get(&parent).unwrap()
};
let new_child = parent_node.create_child(name);
nodes.insert(id, new_child);
});
responder.send(TestResult::Ok)?;
}
Action::CreateNumericProperty(CreateNumericProperty {
parent,
id,
name,
value: Value::IntT(value),
}) => {
inspector_maybe.as_ref().map(|i| {
let parent_node = if parent == 0 {
i.root()
} else {
nodes.get(&parent).unwrap()
};
properties.insert(id, parent_node.create_int(name, value))
});
responder.send(TestResult::Ok)?;
}
Action::CreateNumericProperty(CreateNumericProperty {
value: Value::UintT(_),
..
}) => {
responder.send(TestResult::Unimplemented)?;
}
_ => responder.send(TestResult::Illegal)?,
},
InspectPuppetRequest::InitializeTree { params: _, responder } => {
responder.send(None, TestResult::Unimplemented)?;
}
InspectPuppetRequest::ActLazy { lazy_action: _, responder } => {
responder.send(TestResult::Unimplemented)?;
}
InspectPuppetRequest::Publish { responder } => {
responder.send(TestResult::Unimplemented)?;
}
InspectPuppetRequest::_UnknownMethod { .. } => {}
}
}
Ok(())
}
.unwrap_or_else(|e: anyhow::Error| info!("error running validate interface: {:?}", e)),
)
.detach();
}
}