blob: a51868a5da94e2263cabe3fc6aca1dfc34335d3e [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::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();
}
}