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