blob: 4be0ab8460a060e7eb38f7304b7aa7fb1df0ad32 [file] [log] [blame]
// Copyright 2019 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},
fidl_fuchsia_diagnostics::Selector,
fuchsia_inspect as inspect, json5,
selectors::parse_selector_file,
serde::Deserialize,
std::path::{Path, PathBuf},
std::{collections::BTreeMap, fs},
};
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct Config {
/// Path to which archived data will be written. No storage will be performed if left empty.
pub archive_path: Option<PathBuf>,
/// The maximum size the archive can be.
pub max_archive_size_bytes: u64,
/// The maximum size of a single event file group.
pub max_event_group_size_bytes: u64,
/// Number of threads the archivist has available to use.
pub num_threads: usize,
/// Paths to summarize in our own diagnostics output.
pub summarized_dirs: Option<BTreeMap<String, String>>,
}
/// Configuration for pipeline selection.
pub struct PipelineConfig {
/// Vector of file paths to inspect pipeline configurations.
inspect_configs: Vec<PathBuf>,
/// The selectors parsed from this config.
inspect_selectors: Option<Vec<Selector>>,
/// Accumulated errors from reading config files.
errors: Vec<String>,
}
impl PipelineConfig {
/// Read a pipeline config from the given directory.
pub fn from_directory(dir: impl AsRef<Path>) -> Self {
let suffix = std::ffi::OsStr::new("cfg");
let mut inspect_configs = vec![];
let mut inspect_selectors = Some(vec![]);
let mut errors = vec![];
let readdir = dir.as_ref().read_dir();
match readdir {
Err(_) => {
errors.push(format!("Failed to read directory {}", dir.as_ref().to_string_lossy()));
}
Ok(mut readdir) => {
while let Some(Ok(entry)) = readdir.next() {
let path = entry.path();
if path.extension() == Some(&suffix) {
match parse_selector_file(&path) {
Ok(selectors) => {
inspect_selectors.as_mut().unwrap().extend(selectors);
inspect_configs.push(path);
}
Err(e) => {
errors.push(format!(
"Failed to parse {}: {}",
path.to_string_lossy(),
e.to_string()
));
}
}
}
}
}
}
Self { inspect_configs, inspect_selectors, errors }
}
/// Take the inspect selectors from this pipeline config.
pub fn take_inspect_selectors(&mut self) -> Option<Vec<Selector>> {
self.inspect_selectors.take()
}
/// Record stats about this pipeline config to an Inspect Node.
pub fn record_to_inspect(&self, node: &inspect::Node) {
let files = node.create_child("config_files");
for name in self.inspect_configs.iter() {
let c = files.create_child(name.file_stem().unwrap_or_default().to_string_lossy());
files.record(c);
}
node.record(files);
if self.errors.len() != 0 {
let errors = node.create_child("errors");
for (i, error) in self.errors.iter().enumerate() {
let error_node = errors.create_child(format!("{}", i));
error_node.record_string("message", error);
errors.record(error_node);
}
node.record(errors);
}
}
/// Returns true if this pipeline config had errors.
pub fn has_error(&self) -> bool {
self.errors.len() > 0
}
}
pub fn parse_config(path: impl Into<PathBuf>) -> Result<Config, Error> {
let path: PathBuf = path.into();
let json_string: String = fs::read_to_string(path)?;
let config: Config = json5::from_str(&json_string)?;
if let Some(summarized_dirs) = &config.summarized_dirs {
if summarized_dirs.iter().any(|(dir, _)| dir == "archive") {
return Err(format_err!("Invalid name 'archive' in summarized dirs"));
}
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
use fuchsia_inspect::testing::{assert_inspect_tree, AnyProperty};
use std::io::Write;
use std::path::Path;
fn write_test_config_to_file<T: AsRef<Path>>(path: T, test_config: &str) {
let mut file = fs::File::create(path).expect("failed to create file");
write!(file, "{}", test_config).expect("failed to write file");
file.sync_all().expect("failed to sync file");
}
#[test]
fn parse_config_with_invalid_summarized_dir() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
// Test comment for json5 portability.
"max_archive_size_bytes": 10485760,
"max_event_group_size_bytes": 262144,
"num_threads": 4,
"summarized_dirs": {
"archive": "/data/archive"
}
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config_result = parse_config(&test_config_file_name);
assert!(parsed_config_result.is_err());
}
#[test]
fn parse_valid_config() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
// Test comment for json5 portability.
"max_archive_size_bytes": 10485760,
"max_event_group_size_bytes": 262144,
"num_threads": 4,
"summarized_dirs": {
"global_data": "/global_data",
"global_tmp": "/global_tmp"
}
}"#;
let mut expected_dirs: BTreeMap<String, String> = BTreeMap::new();
expected_dirs.insert("global_data".into(), "/global_data".into());
expected_dirs.insert("global_tmp".into(), "/global_tmp".into());
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config = parse_config(&test_config_file_name).unwrap();
assert_eq!(parsed_config.max_archive_size_bytes, 10485760);
assert_eq!(parsed_config.max_event_group_size_bytes, 262144);
assert_eq!(parsed_config.num_threads, 4);
assert_eq!(parsed_config.summarized_dirs, Some(expected_dirs));
}
#[test]
fn parse_valid_config_missing_optional() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
// Test comment for json5 portability.
"max_archive_size_bytes": 10485760,
"max_event_group_size_bytes": 262144,
"num_threads": 1
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config = parse_config(&test_config_file_name).unwrap();
assert_eq!(parsed_config.max_archive_size_bytes, 10485760);
assert_eq!(parsed_config.max_event_group_size_bytes, 262144);
assert_eq!(parsed_config.num_threads, 1);
}
#[test]
fn parse_invalid_config() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
"max_archive_size_bytes": 10485760,
"bad_field": "hello world",
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config_result = parse_config(&test_config_file_name);
assert!(parsed_config_result.is_err(), "Config had a missing field, and invalid field.");
}
#[test]
fn parse_missing_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let config = PipelineConfig::from_directory("config/missing");
assert!(config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
errors: {
"0": {
message: "Failed to read directory config/missing"
}
},
config_files: {}
});
}
#[test]
fn parse_partially_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("ok.cfg"), "my_component.cmx:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(config_path.join("bad.cfg"), "This file fails to parse").unwrap();
let mut config = PipelineConfig::from_directory(&config_path);
assert!(config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
errors: {
"0": {
message: AnyProperty
}
},
config_files: {
ok: {},
}
});
assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
}
#[test]
fn parse_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("ok.cfg"), "my_component.cmx:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(
config_path.join("also_ok.cfg"),
"my_component.cmx:root:a\nmy_component.cmx:root/b:c\n",
)
.unwrap();
let mut config = PipelineConfig::from_directory(&config_path);
assert!(!config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
config_files: {
ok: {},
also_ok: {}
}
});
assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
}
}