| // 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(); |
| } |
| } |