blob: 7bba45793fe6b250adb86d52e340a9f0036a50de [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 {
anyhow::{format_err, Error},
fuchsia_inspect::{self as inspect, NumericProperty},
fuchsia_zircon as zx,
futures::{FutureExt, StreamExt},
std::{collections::BTreeMap, path::PathBuf},
};
/// Publish stats on global storage as inspect file nodes at the configured paths.
pub fn add_stats_nodes(
root_node: &inspect::Node,
dirs: BTreeMap<String, String>,
) -> Result<(), Error> {
let stats_node = root_node.create_child("data_stats");
for (name, directory_path) in dirs {
stats_node.record_lazy_child(name.clone(), move || {
let name_clone = name.clone();
let directory_path_clone = directory_path.clone();
async move {
let dir_path = PathBuf::from(directory_path_clone);
get_data_directory_stats(name_clone, dir_path)
.await
.map_err(|e| format_err!("Error: {:?}", e))
}
.boxed()
});
}
root_node.record(stats_node);
Ok(())
}
async fn get_filesystem_info_from_admin(
proxy: fidl_fuchsia_io::DirectoryAdminProxy,
) -> Result<fidl_fuchsia_io::FilesystemInfo, String> {
let (status, info) =
proxy.query_filesystem().await.or_else(|_| Err("Query failed".to_string()))?;
if status != 0 {
return Err("Query returned error".to_string());
}
info.ok_or_else(|| "FilesystemInfo wasn't present".to_string()).map(|v| *v)
}
/// Traverses `path` recursively and populates the returned inspect VMO with stats on the contents.
async fn get_data_directory_stats(
name: String,
path: PathBuf,
) -> Result<inspect::Inspector, zx::Status> {
let inspector = inspect::Inspector::new_with_size(1024 * 1024); // 1MB buffer.
let proxy = io_util::open_directory_in_namespace(
&path.to_string_lossy(),
fidl_fuchsia_io::OPEN_RIGHT_READABLE,
)
.or(Err(zx::Status::INTERNAL))?;
let mut file_and_size = files_async::readdir_recursive(&proxy, None)
.filter_map(|result| async {
match result {
Ok(entry) => {
let pb = PathBuf::from(entry.name);
let maybe_meta = path.join(&pb).metadata().ok();
if let Some(meta) = maybe_meta {
if meta.is_file() {
return Some((pb, meta.len()));
}
}
None
}
_ => {
// Errors occur when reading dirs. We skip reading this dir.
// TODO(fxb/49157): consider showing an error in the output if we failed to
// read this dir.
None
}
}
})
.collect::<Vec<_>>()
.await;
file_and_size.sort_by(|a, b| a.0.cmp(&b.0));
struct Entry {
node: inspect::Node,
size_property: inspect::UintProperty,
children: BTreeMap<String, Box<Entry>>,
}
let node = inspector.root().create_child(name);
let size_property = node.create_uint("size", 0);
let mut top = Box::new(Entry { node, size_property, children: BTreeMap::new() });
for (pb, size) in file_and_size.iter() {
top.size_property.add(*size);
let mut cur = &mut top;
for next in pb {
let next = next.to_string_lossy().into_owned();
if !cur.children.contains_key(&next) {
let node = cur.node.create_child(&next);
let size_property = node.create_uint("size", 0);
cur.children.insert(
next.clone(),
Box::new(Entry { node, size_property, children: BTreeMap::new() }),
);
}
cur = cur.children.get_mut(&next).expect("find just inserted value");
cur.size_property.add(*size);
}
}
let mut entries = vec![top];
while let Some(entry) = entries.pop() {
entry.node.record(entry.size_property);
inspector.root().record(entry.node);
for (_, child_entry) in entry.children {
entries.push(child_entry);
}
}
async fn get_info(path: String) -> Result<fidl_fuchsia_io::FilesystemInfo, String> {
let proxy = io_util::open_directory_in_namespace(
&path,
fidl_fuchsia_io::OPEN_RIGHT_READABLE | fidl_fuchsia_io::OPEN_RIGHT_ADMIN,
)
.or_else(|e| Err(format!("Could not get admin access {:?}", e)))?;
let proxy = fidl_fuchsia_io::DirectoryAdminProxy::new(
proxy.into_channel().or_else(|e| Err(format!("Could not extract channel {:?}", e)))?,
);
get_filesystem_info_from_admin(proxy).await
}
let stats = inspector.root().create_child("stats");
match get_info(path.to_string_lossy().to_string()).await {
Ok(info) => {
add_filesystem_stats(&stats, info);
}
Err(val) => {
stats.record_string("error", val);
}
};
inspector.root().record(stats);
Ok(inspector)
}
fn add_filesystem_stats(stats: &inspect::Node, info: fidl_fuchsia_io::FilesystemInfo) {
// Total bytes includes the actual size of the filesystem and shared bytes that can be used between
// filesystems. We aren't out of space until we've exhausted both.
stats.record_uint("total_bytes", info.total_bytes + info.free_shared_pool_bytes);
stats.record_uint("used_bytes", info.used_bytes);
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::endpoints::create_proxy,
fuchsia_async as fasync,
fuchsia_inspect::{assert_inspect_tree, reader},
futures::TryStreamExt,
std::{fs::File, io::prelude::*},
tempfile::TempDir,
};
#[fasync::run_singlethreaded(test)]
async fn get_data_directory_stats_test() {
let tempdir = TempDir::new().expect("failed to create tmp dir");
File::create(tempdir.path().join("my_file.txt"))
.expect("create file")
.write_all(b"Hello")
.expect("writing test file");
std::fs::create_dir_all(tempdir.path().join("data/a/b")).expect("make data/a/b");
std::fs::create_dir_all(tempdir.path().join("data/a/c/d")).expect("make data/a/c/d");
for f in
["data/top.txt", "data/a/a.txt", "data/a/b/b.txt", "data/a/c/c.txt", "data/a/c/d/d.txt"]
.iter()
{
File::create(tempdir.path().join(f))
.expect(format!("create file {}", f).as_ref())
.write_all(f.as_bytes())
.expect(format!("writing {}", f).as_ref());
}
let mut dirs = BTreeMap::new();
dirs.insert(
"test_data".to_string(),
tempdir.path().join("data").to_string_lossy().to_string(),
);
let inspector = inspect::Inspector::new();
add_stats_nodes(inspector.root(), dirs).expect("get data");
let hierarchy = reader::read_from_inspector(&inspector).await.expect("get hierarchy");
assert_inspect_tree!(hierarchy,
root: {
data_stats: {
test_data: {
stats: contains {
error: "Query failed"
},
test_data: {
size: 68u64,
"top.txt": {
size: 12u64
},
a: {
size: 56u64,
"a.txt": {
size: 12u64
},
b: {
size: 14u64,
"b.txt": {
size: 14u64,
}
},
c: {
size: 30u64,
"c.txt": {
size: 14u64
},
d: {
size: 16u64,
"d.txt": {
size: 16u64
}
}
}
}
}
}
}
});
}
#[fasync::run_singlethreaded(test)]
async fn publish_data_directory_stats_test() {
let tempdir = TempDir::new().expect("failed to create tmp dir");
File::create(tempdir.path().join("test.txt")).expect("make file");
let mut dirs = BTreeMap::new();
dirs.insert("test_data".to_string(), tempdir.path().to_string_lossy().to_string());
let inspector = inspect::Inspector::new();
add_stats_nodes(inspector.root(), dirs).expect("get data");
let hierarchy = reader::read_from_inspector(&inspector).await.expect("get hierarchy");
assert_inspect_tree!(hierarchy,
root: {
data_stats: {
test_data: {
stats: contains {
error: "Query failed"
},
test_data: contains {
size:0u64,
"test.txt": {
size: 0u64
}
}
}
}
});
}
#[fasync::run_singlethreaded(test)]
async fn get_filesystem_info_from_admin_test() {
let (proxy, server) = create_proxy::<fidl_fuchsia_io::DirectoryAdminMarker>()
.expect("failed to create proxy");
fasync::spawn(async move {
let mut stream = server.into_stream().expect("failed to convert to stream");
while let Some(request) = stream.try_next().await.expect("failed to unwrap request") {
match request {
fidl_fuchsia_io::DirectoryAdminRequest::QueryFilesystem { responder } => {
responder
.send(
0,
Some(&mut fidl_fuchsia_io::FilesystemInfo {
total_bytes: 800,
used_bytes: 200,
total_nodes: 0,
used_nodes: 0,
free_shared_pool_bytes: 200,
fs_id: 0,
block_size: 4096,
max_filename_size: 128,
fs_type: 0,
padding: 0,
name: ['a' as i8; 32],
}),
)
.expect("failed to send response");
}
r => {
panic!("Unknown request {:?}", r);
}
}
}
});
let info =
get_filesystem_info_from_admin(proxy).await.expect("failed to get filesystem info");
let inspector = inspect::Inspector::new();
add_filesystem_stats(inspector.root(), info);
let hierarchy = reader::read_from_inspector(&inspector).await.expect("get hierarchy");
assert_inspect_tree!(hierarchy,
root: {
total_bytes: 1000u64,
used_bytes: 200u64
});
}
#[fasync::run_singlethreaded(test)]
async fn get_filesystem_info_from_admin_failure_test() {
let (proxy, server) = create_proxy::<fidl_fuchsia_io::DirectoryAdminMarker>()
.expect("failed to create proxy");
fasync::spawn(async move {
let mut stream = server.into_stream().expect("failed to convert to stream");
while let Some(request) = stream.try_next().await.expect("failed to unwrap request") {
match request {
fidl_fuchsia_io::DirectoryAdminRequest::QueryFilesystem { responder } => {
responder.send(1, None).expect("failed to send response");
}
r => {
panic!("Unknown request {:?}", r);
}
}
}
});
assert_eq!(
Err("Query returned error".to_string()),
get_filesystem_info_from_admin(proxy).await
);
}
}