blob: 8f5b26e562e896ea9bd379910c3183cb9c4d07d5 [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,
},
anyhow::{format_err, Context as _, Error},
fidl_fuchsia_inspect as fidl_inspect,
fidl_fuchsia_sys::{EnvironmentControllerProxy, EnvironmentMarker, EnvironmentOptions},
fidl_test_inspect_validate as validate,
fuchsia_component::client as fclient,
fuchsia_url::pkg_url::PkgUrl,
fuchsia_zircon::{self as zx, Vmo},
std::sync::atomic::{AtomicU64, Ordering},
std::{convert::TryFrom, path::Path, str::FromStr},
};
pub const VMO_SIZE: u64 = 4096;
/// Create a unique environment name with the given prefix.
fn make_environment_name(prefix: impl AsRef<str>) -> String {
static NEXT_ENVIRONMENT_SUFFIX: AtomicU64 = AtomicU64::new(0);
let v = NEXT_ENVIRONMENT_SUFFIX.fetch_add(1, Ordering::Relaxed);
format!("{}_{}", prefix.as_ref(), v)
}
pub struct Puppet {
pub vmo: Vmo,
// Need to remember the connection to avoid dropping the VMO
connection: Connection,
name: String,
}
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 unpublish(&mut self) -> Result<validate::TestResult, Error> {
Ok(self.connection.fidl.unpublish().await?)
}
pub fn name<'a>(&'a self) -> &'a str {
&self.name
}
pub fn component_name(&self) -> String {
format!("{}.cmx", self.name())
}
// Extracts the .cmx file basename for output to the user.
fn derive_my_name(url: &str) -> Result<String, Error> {
let url_parse = PkgUrl::from_str(url)?;
let cmx_name = url_parse.resource().ok_or(format_err!("URL parse"))?;
let cmx_path = Path::new(cmx_name);
if let Some(s) = cmx_path.file_stem() {
if let Some(s) = s.to_str() {
return Ok(s.to_owned());
}
}
return Err(format_err!("Bad path {} from url {}", cmx_name, url));
}
pub async fn connect(server_url: &str) -> Result<Self, Error> {
Puppet::initialize_with_connection(
Connection::start_and_connect(server_url).await?,
Self::derive_my_name(server_url)?,
)
.await
}
/// Get the environment name the puppet was run in.
pub fn environment_name<'a>(&'a self) -> &'a str {
&self.connection.environment_name
}
#[cfg(test)]
pub async fn connect_local(local_fidl: validate::ValidateProxy) -> Result<Puppet, Error> {
Puppet::initialize_with_connection(
Connection::new(local_fidl, None, None, "".to_owned()),
"*Local*".to_owned(),
)
.await
}
async fn initialize_with_connection(
mut connection: Connection,
name: String,
) -> Result<Puppet, Error> {
Ok(Puppet { vmo: connection.initialize_vmo().await?, connection, name })
}
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::ValidateProxy,
// Connection to Tree FIDL if Puppet supports it.
// Puppets can add support by implementing InitializeTree method.
root_link_channel: Option<fidl_inspect::TreeProxy>,
// We need to keep the 'app' un-dropped on non-local connections so the
// remote program doesn't go away. But we never use it once we have the
// FIDL connection.
_app: Option<fuchsia_component::client::App>,
// The nested environment we are starting the puppet in.
_env: Option<EnvironmentControllerProxy>,
// The name of the environment we started the component in.
pub environment_name: String,
}
impl Connection {
// Note! In v1, the launch() and connect_to_service() functions do not return errors
// when given a bad URL. There's no way to detect bad URLs until we actually make a
// FIDL call that the server is supposed to serve, in initialize_vmo().
async fn start_and_connect(server_url: impl Into<String>) -> Result<Self, Error> {
let server_url = server_url.into();
let (new_env, new_env_server_end) = fidl::endpoints::create_proxy()?;
let (controller, controller_server_end) = fidl::endpoints::create_proxy()?;
let (launcher, launcher_server_end) = fidl::endpoints::create_proxy()?;
let env = fclient::connect_to_service::<EnvironmentMarker>()?;
let environment_name = make_environment_name("puppet");
env.create_nested_environment(
new_env_server_end,
controller_server_end,
&environment_name,
None,
&mut EnvironmentOptions {
inherit_parent_services: true,
use_parent_runners: false,
kill_on_oom: false,
delete_storage_on_death: false,
},
)
.context("creating isolated environment")?;
new_env.get_launcher(launcher_server_end).context("getting nested environment launcher")?;
let app = fclient::launch(&launcher, server_url.clone(), None)
.context(format!("Failed to launch Validator puppet {}", server_url))?;
let puppet_fidl = app
.connect_to_service::<validate::ValidateMarker>()
.context("Failed to connect to validate puppet")?;
Ok(Self::new(puppet_fidl, Some(app), Some(controller), environment_name))
}
async fn fetch_link_channel(fidl: &validate::ValidateProxy) -> Option<fidl_inspect::TreeProxy> {
let params = validate::InitializationParams {
vmo_size: Some(VMO_SIZE),
..validate::InitializationParams::EMPTY
};
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::ValidateProxy,
app: Option<fuchsia_component::client::App>,
env: Option<EnvironmentControllerProxy>,
environment_name: String,
) -> Self {
Self { fidl, root_link_channel: None, _app: app, _env: env, environment_name }
}
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),
..validate::InitializationParams::EMPTY
};
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, DiffType},
//anyhow::format_err,
fidl::endpoints::{create_proxy, RequestStream, ServerEnd},
fidl_test_inspect_validate::*,
fuchsia_async as fasync,
fuchsia_inspect::{Inspector, IntProperty, Node},
fuchsia_zircon::HandleBased,
futures::prelude::*,
log::*,
std::collections::HashMap,
};
#[test]
fn puppet_name_derivation() -> Result<(), Error> {
assert_eq!(
Puppet::derive_my_name("fuchsia-pkg://path.com/name#meta/my_name.cmx")?,
"my_name".to_string()
);
Ok(())
}
#[fasync::run_singlethreaded(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<ValidateMarker>) {
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 = ValidateRequestStream::from_channel(server_chan);
while let Some(event) = stream.try_next().await? {
match event {
ValidateRequest::Initialize { params, responder } => {
let inspector = match params.vmo_size {
Some(size) => Inspector::new_with_size(size as usize),
None => Inspector::new(),
};
responder
.send(
inspector.duplicate_vmo().map(|v| v.into_handle()),
TestResult::Ok,
)
.context("responding to initialize")?;
inspector_maybe = Some(inspector);
}
ValidateRequest::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: Number::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: Number::UintT(_),
..
}) => {
responder.send(TestResult::Unimplemented)?;
}
_ => responder.send(TestResult::Illegal)?,
},
ValidateRequest::InitializeTree { params: _, responder } => {
responder.send(None, TestResult::Unimplemented)?;
}
ValidateRequest::ActLazy { lazy_action: _, responder } => {
responder.send(TestResult::Unimplemented)?;
}
ValidateRequest::Publish { responder } => {
responder.send(TestResult::Unimplemented)?;
}
ValidateRequest::Unpublish { responder } => {
responder.send(TestResult::Unimplemented)?;
}
}
}
Ok(())
}
.unwrap_or_else(|e: anyhow::Error| info!("error running validate interface: {:?}", e)),
)
.detach();
}
}