| // 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::events::types::InspectData, |
| anyhow::{format_err, Error}, |
| fidl::endpoints::{DiscoverableService, Proxy}, |
| fidl_fuchsia_inspect::TreeMarker, |
| fidl_fuchsia_inspect_deprecated::InspectMarker, |
| fidl_fuchsia_io::{DirectoryProxy, NodeInfo}, |
| files_async, |
| futures::future::BoxFuture, |
| futures::stream::StreamExt, |
| futures::{FutureExt, TryFutureExt}, |
| io_util, |
| parking_lot::Mutex, |
| pin_utils::pin_mut, |
| std::collections::HashMap, |
| std::path::{Path, PathBuf}, |
| std::sync::Arc, |
| tracing::error, |
| }; |
| |
| /// Mapping from a diagnostics filename to the underlying encoding of that |
| /// diagnostics data. |
| pub type DataMap = HashMap<String, InspectData>; |
| |
| pub type Moniker = String; |
| |
| /// InspectDataCollector holds the information needed to retrieve the Inspect |
| /// VMOs associated with a particular component |
| #[derive(Clone, Debug)] |
| pub struct InspectDataCollector { |
| /// The inspect data associated with a particular event. |
| /// |
| /// This is wrapped in an Arc Mutex so it can be shared between multiple data sources. |
| /// |
| /// Note: The Arc is needed so that we can both add the data map to a data collector |
| /// and trigger async collection of the data in the same method. This can only |
| /// be done by allowing the async method to populate the same data that is being |
| /// passed into the component event. |
| inspect_data_map: Arc<Mutex<Option<DataMap>>>, |
| } |
| |
| impl InspectDataCollector { |
| /// Construct a new InspectDataCollector, wrapped by an Arc<Mutex>. |
| pub fn new() -> Self { |
| InspectDataCollector { inspect_data_map: Arc::new(Mutex::new(Some(DataMap::new()))) } |
| } |
| |
| /// Convert a fully-qualified path to a directory-proxy in the executing namespace. |
| /// NOTE: Currently does a synchronous directory-open, since there are no available |
| /// async apis. |
| pub async fn find_directory_proxy(path: &Path) -> Result<DirectoryProxy, Error> { |
| // TODO(fxbug.dev/36762): When available, use the async directory-open api. |
| return io_util::open_directory_in_namespace( |
| &path.to_string_lossy(), |
| io_util::OPEN_RIGHT_READABLE | io_util::OPEN_RIGHT_WRITABLE, |
| ); |
| } |
| |
| /// Searches the directory specified by inspect_directory_proxy for |
| /// .inspect files and populates the `inspect_data_map` with the found VMOs. |
| pub async fn populate_data_map(&mut self, inspect_proxy: &DirectoryProxy) -> Result<(), Error> { |
| // TODO(fxbug.dev/36762): Use a streaming and bounded readdir API when available to avoid |
| // being hung. |
| let entries = files_async::readdir_recursive(inspect_proxy, /* timeout= */ None) |
| .filter_map(|result| { |
| async move { |
| // TODO(fxbug.dev/49157): decide how to show directories that we failed to read. |
| result.ok() |
| } |
| }); |
| pin_mut!(entries); |
| // TODO(fxbug.dev/60250) convert this async loop to a stream so we can carry backpressure |
| while let Some(entry) = entries.next().await { |
| // We are only currently interested in inspect VMO files (root.inspect) and |
| // inspect services. |
| if let Ok(Some(proxy)) = self.maybe_load_service::<TreeMarker>(inspect_proxy, &entry) { |
| let maybe_vmo = |
| proxy.get_content().err_into::<anyhow::Error>().await?.buffer.map(|b| b.vmo); |
| |
| self.maybe_add(&entry.name, InspectData::Tree(proxy, maybe_vmo)); |
| continue; |
| } |
| |
| if let Ok(Some(proxy)) = self.maybe_load_service::<InspectMarker>(inspect_proxy, &entry) |
| { |
| self.maybe_add(&entry.name, InspectData::DeprecatedFidl(proxy)); |
| continue; |
| } |
| |
| if !entry.name.ends_with(".inspect") || entry.kind != files_async::DirentKind::File { |
| continue; |
| } |
| |
| let file_proxy = match io_util::open_file( |
| inspect_proxy, |
| Path::new(&entry.name), |
| io_util::OPEN_RIGHT_READABLE, |
| ) { |
| Ok(proxy) => proxy, |
| Err(_) => { |
| continue; |
| } |
| }; |
| |
| // Obtain the vmo backing any VmoFiles. |
| match file_proxy.describe().err_into::<anyhow::Error>().await { |
| Ok(nodeinfo) => match nodeinfo { |
| NodeInfo::Vmofile(vmofile) => { |
| self.maybe_add(&entry.name, InspectData::Vmo(vmofile.vmo)); |
| } |
| NodeInfo::File(_) => { |
| let contents = io_util::read_file_bytes(&file_proxy).await?; |
| self.maybe_add(&entry.name, InspectData::File(contents)); |
| } |
| ty @ _ => { |
| error!( |
| file = %entry.name, ?ty, |
| "found an inspect file of unexpected type", |
| ); |
| } |
| }, |
| Err(_) => {} |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Adds a key value to the contained vector if it hasn't been taken yet. Otherwise, does |
| /// nothing. |
| fn maybe_add(&mut self, key: impl Into<String>, value: InspectData) { |
| if let Some(map) = self.inspect_data_map.lock().as_mut() { |
| map.insert(key.into(), value); |
| }; |
| } |
| |
| fn maybe_load_service<S: DiscoverableService>( |
| &self, |
| dir_proxy: &DirectoryProxy, |
| entry: &files_async::DirEntry, |
| ) -> Result<Option<S::Proxy>, Error> { |
| if entry.name.ends_with(S::SERVICE_NAME) { |
| let (proxy, server) = fidl::endpoints::create_proxy::<S>()?; |
| fdio::service_connect_at( |
| dir_proxy.as_channel().as_ref(), |
| &entry.name, |
| server.into_channel(), |
| )?; |
| return Ok(Some(proxy)); |
| } |
| Ok(None) |
| } |
| |
| /// Takes the contained extra data. Additions following this have no effect. |
| pub fn take_data(self: Box<Self>) -> Option<DataMap> { |
| self.inspect_data_map.lock().take() |
| } |
| |
| /// Collect extra data stored under the given path. |
| /// |
| /// This currently only does a single pass over the directory to find information. |
| pub fn collect(mut self: Box<Self>, path: PathBuf) -> BoxFuture<'static, Result<(), Error>> { |
| async move { |
| let inspect_proxy = match InspectDataCollector::find_directory_proxy(&path).await { |
| Ok(proxy) => proxy, |
| Err(e) => { |
| return Err(format_err!("Failed to open out directory at {:?}: {}", path, e)); |
| } |
| }; |
| |
| self.populate_data_map(&inspect_proxy).await |
| } |
| .boxed() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::events::types::InspectData, |
| diagnostics_hierarchy::DiagnosticsHierarchy, |
| fdio, |
| fidl::endpoints::DiscoverableService, |
| fidl_fuchsia_inspect::TreeMarker, |
| fuchsia_async as fasync, |
| fuchsia_component::server::ServiceFs, |
| fuchsia_inspect::reader::PartialNodeHierarchy, |
| fuchsia_inspect::{assert_inspect_tree, reader, Inspector}, |
| fuchsia_zircon as zx, |
| fuchsia_zircon::Peered, |
| futures::{FutureExt, StreamExt}, |
| std::convert::TryFrom, |
| std::path::PathBuf, |
| }; |
| |
| fn get_vmo(text: &[u8]) -> zx::Vmo { |
| let vmo = zx::Vmo::create(4096).unwrap(); |
| vmo.write(text, 0).unwrap(); |
| vmo |
| } |
| |
| #[fuchsia::test] |
| async fn inspect_data_collector() { |
| let path = PathBuf::from("/test-bindings"); |
| // Make a ServiceFs containing two files. |
| // One is an inspect file, and one is not. |
| let mut fs = ServiceFs::new(); |
| let vmo = get_vmo(b"test1"); |
| let vmo2 = get_vmo(b"test2"); |
| let vmo3 = get_vmo(b"test3"); |
| let vmo4 = get_vmo(b"test4"); |
| fs.dir("diagnostics").add_vmo_file_at("root.inspect", vmo, 0, 4096); |
| fs.dir("diagnostics").add_vmo_file_at("root_not_inspect", vmo2, 0, 4096); |
| fs.dir("diagnostics").dir("a").add_vmo_file_at("root.inspect", vmo3, 0, 4096); |
| fs.dir("diagnostics").dir("b").add_vmo_file_at("root.inspect", vmo4, 0, 4096); |
| // Create a connection to the ServiceFs. |
| let (h0, h1) = zx::Channel::create().unwrap(); |
| fs.serve_connection(h1).unwrap(); |
| |
| let ns = fdio::Namespace::installed().unwrap(); |
| ns.bind(path.join("out").to_str().unwrap(), h0).unwrap(); |
| |
| fasync::Task::spawn(fs.collect()).detach(); |
| |
| let (done0, done1) = zx::Channel::create().unwrap(); |
| |
| let thread_path = path.join("out/diagnostics"); |
| |
| // Run the actual test in a separate thread so that it does not block on FS operations. |
| // Use signalling on a zx::Channel to indicate that the test is done. |
| std::thread::spawn(move || { |
| let path = thread_path; |
| let done = done1; |
| let mut executor = fasync::Executor::new().unwrap(); |
| |
| executor.run_singlethreaded(async { |
| let collector = InspectDataCollector::new(); |
| // Trigger collection on a clone of the inspect collector so |
| // we can use collector to take the collected data. |
| Box::new(collector.clone()).collect(path).await.unwrap(); |
| let collector: Box<InspectDataCollector> = Box::new(collector); |
| |
| let extra_data = collector.take_data().expect("collector missing data"); |
| assert_eq!(3, extra_data.len()); |
| |
| let assert_extra_data = |path: &str, content: &[u8]| { |
| let extra = extra_data.get(path); |
| assert!(extra.is_some()); |
| |
| match extra.unwrap() { |
| InspectData::Vmo(vmo) => { |
| let mut buf = [0u8; 5]; |
| vmo.read(&mut buf, 0).expect("reading vmo"); |
| assert_eq!(content, &buf); |
| } |
| v => { |
| panic!("Expected Vmo, got {:?}", v); |
| } |
| } |
| }; |
| |
| assert_extra_data("root.inspect", b"test1"); |
| assert_extra_data("a/root.inspect", b"test3"); |
| assert_extra_data("b/root.inspect", b"test4"); |
| |
| done.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).expect("signalling peer"); |
| }); |
| }); |
| |
| fasync::OnSignals::new(&done0, zx::Signals::USER_0).await.unwrap(); |
| ns.unbind(path.join("out").to_str().unwrap()).unwrap(); |
| } |
| |
| #[fuchsia::test] |
| async fn inspect_data_collector_tree() { |
| let path = PathBuf::from("/test-bindings2"); |
| |
| // Make a ServiceFs serving an inspect tree. |
| let mut fs = ServiceFs::new(); |
| let inspector = Inspector::new(); |
| inspector.root().record_int("a", 1); |
| inspector.root().record_lazy_child("lazy", || { |
| async move { |
| let inspector = Inspector::new(); |
| inspector.root().record_double("b", 3.14); |
| Ok(inspector) |
| } |
| .boxed() |
| }); |
| inspector.serve(&mut fs).expect("failed to serve inspector"); |
| |
| // Create a connection to the ServiceFs. |
| let (h0, h1) = zx::Channel::create().unwrap(); |
| fs.serve_connection(h1).unwrap(); |
| |
| let ns = fdio::Namespace::installed().unwrap(); |
| ns.bind(path.join("out").to_str().unwrap(), h0).unwrap(); |
| |
| fasync::Task::spawn(fs.collect()).detach(); |
| |
| let (done0, done1) = zx::Channel::create().unwrap(); |
| let thread_path = path.join("out/diagnostics"); |
| |
| // Run the actual test in a separate thread so that it does not block on FS operations. |
| // Use signalling on a zx::Channel to indicate that the test is done. |
| std::thread::spawn(move || { |
| let path = thread_path; |
| let done = done1; |
| let mut executor = fasync::Executor::new().unwrap(); |
| |
| executor.run_singlethreaded(async { |
| let collector = InspectDataCollector::new(); |
| |
| //// Trigger collection on a clone of the inspect collector so |
| //// we can use collector to take the collected data. |
| Box::new(collector.clone()).collect(path).await.unwrap(); |
| let collector: Box<InspectDataCollector> = Box::new(collector); |
| |
| let extra_data = collector.take_data().expect("collector missing data"); |
| assert_eq!(1, extra_data.len()); |
| |
| let extra = extra_data.get(TreeMarker::SERVICE_NAME); |
| assert!(extra.is_some()); |
| |
| match extra.unwrap() { |
| InspectData::Tree(tree, vmo) => { |
| // Assert we can read the tree proxy and get the data we expected. |
| let hierarchy = |
| reader::read(tree).await.expect("failed to read hierarchy from tree"); |
| assert_inspect_tree!(hierarchy, root: { |
| a: 1i64, |
| lazy: { |
| b: 3.14, |
| } |
| }); |
| let partial_hierarchy: DiagnosticsHierarchy = |
| PartialNodeHierarchy::try_from(vmo.as_ref().unwrap()) |
| .expect("failed to read hierarchy from vmo") |
| .into(); |
| // Assert the vmo also points to that data (in this case since there's no |
| // lazy nodes). |
| assert_inspect_tree!(partial_hierarchy, root: { |
| a: 1i64, |
| }); |
| } |
| v => { |
| panic!("Expected Tree, got {:?}", v); |
| } |
| } |
| |
| done.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).expect("signalling peer"); |
| }); |
| }); |
| |
| fasync::OnSignals::new(&done0, zx::Signals::USER_0).await.unwrap(); |
| ns.unbind(path.join("out").to_str().unwrap()).unwrap(); |
| } |
| } |