blob: cf776f976df33c54d0c41f40243269605a508da4 [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::ImmutableString,
anyhow::Error,
fidl::endpoints::{DiscoverableProtocolMarker, Proxy},
fidl_fuchsia_inspect::{TreeMarker, TreeProxy},
fidl_fuchsia_inspect_deprecated::{InspectMarker, InspectProxy},
fidl_fuchsia_io::{DirectoryProxy, NodeInfo},
files_async, fuchsia_zircon as zx,
futures::stream::StreamExt,
futures::TryFutureExt,
io_util,
pin_utils::pin_mut,
std::collections::HashMap,
std::path::Path,
tracing::error,
};
#[cfg(test)]
use futures::FutureExt;
/// Mapping from a diagnostics filename to the underlying encoding of that
/// diagnostics data.
pub type DataMap = HashMap<ImmutableString, InspectData>;
pub type Moniker = String;
/// Data associated with a component.
/// This data is stored by data collectors and passed by the collectors to processors.
#[derive(Debug)]
pub enum InspectData {
/// A VMO containing data associated with the event.
Vmo(zx::Vmo),
/// A file containing data associated with the event.
///
/// Because we can't synchronously retrieve file contents like we can for VMOs, this holds
/// the full file contents. Future changes should make streaming ingestion feasible.
File(Vec<u8>),
/// A connection to a Tree service.
Tree(TreeProxy),
/// A connection to the deprecated Inspect service.
DeprecatedFidl(InspectProxy),
}
/// 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.
io_util::open_directory_in_namespace(
&path.to_string_lossy(),
io_util::OPEN_RIGHT_READABLE | io_util::OPEN_RIGHT_WRITABLE,
)
}
fn maybe_load_service<P: DiscoverableProtocolMarker>(
dir_proxy: &DirectoryProxy,
entry: &files_async::DirEntry,
) -> Result<Option<P::Proxy>, Error> {
if entry.name.ends_with(P::PROTOCOL_NAME) {
let (proxy, server) = fidl::endpoints::create_proxy::<P>()?;
fdio::service_connect_at(
dir_proxy.as_channel().as_ref(),
&entry.name,
server.into_channel(),
)?;
return Ok(Some(proxy));
}
Ok(None)
}
/// 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(inspect_proxy: &DirectoryProxy) -> Result<DataMap, 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()
}
});
let mut data_map = DataMap::new();
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)) = maybe_load_service::<TreeMarker>(inspect_proxy, &entry) {
data_map.insert(entry.name.into_boxed_str(), InspectData::Tree(proxy));
continue;
}
if let Ok(Some(proxy)) = maybe_load_service::<InspectMarker>(inspect_proxy, &entry) {
data_map.insert(entry.name.into_boxed_str(), 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) => {
data_map.insert(entry.name.into_boxed_str(), InspectData::Vmo(vmofile.vmo));
}
NodeInfo::File(_) => {
let contents = io_util::read_file_bytes(&file_proxy).await?;
data_map.insert(entry.name.into_boxed_str(), InspectData::File(contents));
}
ty @ _ => {
error!(
file = %entry.name, ?ty,
"found an inspect file of unexpected type",
);
}
},
Err(_) => {}
}
}
Ok(data_map)
}
#[cfg(test)]
pub fn collect(
path: std::path::PathBuf,
) -> futures::future::BoxFuture<'static, Result<DataMap, anyhow::Error>> {
async move {
let inspect_proxy = match find_directory_proxy(&path).await {
Ok(proxy) => proxy,
Err(e) => {
return Err(anyhow::format_err!(
"Failed to open out directory at {:?}: {}",
path,
e
));
}
};
populate_data_map(&inspect_proxy).await
}
.boxed()
}
#[cfg(test)]
mod tests {
use {
super::*,
fdio,
fidl::endpoints::DiscoverableProtocolMarker,
fidl_fuchsia_inspect::TreeMarker,
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_inspect::{assert_data_tree, reader, Inspector},
fuchsia_zircon as zx,
fuchsia_zircon::Peered,
futures::{FutureExt, StreamExt},
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::LocalExecutor::new().unwrap();
executor.run_singlethreaded(async {
let extra_data = collect(path).await.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()
});
inspect_runtime::serve(&inspector, &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::LocalExecutor::new().unwrap();
executor.run_singlethreaded(async {
let extra_data = collect(path).await.expect("collector missing data");
assert_eq!(1, extra_data.len());
let extra = extra_data.get(TreeMarker::PROTOCOL_NAME);
assert!(extra.is_some());
match extra.unwrap() {
InspectData::Tree(tree) => {
// 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_data_tree!(hierarchy, root: {
a: 1i64,
lazy: {
b: 3.14,
}
});
}
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();
}
}